Compare commits

..

40 Commits

Author SHA1 Message Date
github-actions[bot]
2026bb7a9c chore: update versions (#3298)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@8.0.8

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost/react-apollo@17.0.4

### Patch Changes

-   @nhost/apollo@8.0.8
-   @nhost/react@3.10.4

## @nhost/react-urql@14.0.4

### Patch Changes

-   @nhost/react@3.10.4

## @nhost/hasura-auth-js@2.11.1

### Patch Changes

- 5ff4dd6: fix (hasura-auth-js|hasura-storage-js): update e2e config for
packages

## @nhost/hasura-storage-js@2.7.1

### Patch Changes

- 5ff4dd6: fix (hasura-auth-js|hasura-storage-js): update e2e config for
packages

## @nhost/nextjs@2.2.7

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/react@3.10.4

## @nhost/nhost-js@3.2.8

### Patch Changes

-   Updated dependencies [5ff4dd6]
    -   @nhost/hasura-storage-js@2.7.1
    -   @nhost/hasura-auth-js@2.11.1

## @nhost/react@3.10.4

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost/vue@2.9.5

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost/dashboard@2.28.0

### Minor Changes

-   8552678: feat: dashboard: add additional events to segment
-   0bf2808: chore: refresh metadata before end-to-end tests
-   72a365c: fix: correct graphql page roles dropdown's source
-   cef6471: fix: dashboard: add anonid to user's metadata

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
-   233232b: feat (dashboard): improve Upgrade project dialog
-   Updated dependencies [d9eb906]
    -   @nhost/nextjs@2.2.7
    -   @nhost/react-apollo@17.0.4

## @nhost/docs@2.31.0

### Minor Changes

-   b302dbd: feat: added sveltekit quickstart

### Patch Changes

-   5e96230: fix: fixing mintlify breaking our docs

## @nhost-examples/cli@0.3.21

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost-examples/codegen-react-apollo@0.7.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/react@3.10.4
    -   @nhost/react-apollo@17.0.4

## @nhost-examples/codegen-react-query@0.7.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/react@3.10.4

## @nhost-examples/codegen-react-urql@0.6.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/react@3.10.4
    -   @nhost/react-urql@14.0.4

## @nhost-examples/multi-tenant-one-to-many@2.2.22

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost-examples/nextjs@0.4.7

### Patch Changes

-   fad7f64: chore: fix typo
-   d9eb906: fix: update vite and nextjs because of vulnerability
-   Updated dependencies [d9eb906]
    -   @nhost/nextjs@2.2.7
    -   @nhost/react@3.10.4
    -   @nhost/react-apollo@17.0.4

## @nhost-examples/node-storage@0.2.21

### Patch Changes

-   @nhost/nhost-js@3.2.8

## @nhost-examples/nextjs-server-components@0.5.6

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/nhost-js@3.2.8

## @nhost-examples/sveltekit@0.7.1

### Patch Changes

-   f8243f9: chore (examples/svelte): update @sveltejs/kit
-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/nhost-js@3.2.8

## @nhost-examples/react-apollo@1.5.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
- efd68c3: chore (react-apollo): use preview build instead of local dev
server for e2e tests
    -   @nhost/react@3.10.4
    -   @nhost/react-apollo@17.0.4

## @nhost-examples/react-gqty@1.5.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/react@3.10.4

## @nhost-examples/react-native@0.1.8

### Patch Changes

-   @nhost/react@3.10.4
-   @nhost/react-apollo@17.0.4

## @nhost-examples/vue-apollo@0.11.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/nhost-js@3.2.8
    -   @nhost/apollo@8.0.8
    -   @nhost/vue@2.9.5

## @nhost-examples/vue-quickstart@0.5.1

### Patch Changes

-   d9eb906: fix: update vite and nextjs because of vulnerability
    -   @nhost/apollo@8.0.8
    -   @nhost/vue@2.9.5

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-28 13:56:36 +00:00
David Barroso
1bc1e30f5e chore (ci): send message to discord (#3317)
### **PR Type**
Enhancement


___

### **Description**
- Add Discord notifications for dashboard deployment status

- Implement success and failure notifications separately

- Include deployment details in Discord messages

- Use tsickert/discord-webhook action for notifications


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>deploy-dashboard.yaml</strong><dd><code>Implement
Discord notifications for deployment status</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.github/workflows/deploy-dashboard.yaml

<li>Added success notification step using Discord webhook<br> <li> Added
failure notification step using Discord webhook<br> <li> Both
notifications include deployment status, trigger user, and git
<br>ref<br> <li> Used different embed colors for success (green) and
failure (red)


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3317/files#diff-634642357deb8c43286f58a5b454c8f10aeab2fb9937c9cb0c4300ac84dc00cf">+28/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></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>
2025-04-28 14:37:33 +02:00
Nuno Pato
85526782f2 feat: dashboard: add additional segment events (#3313)
### **PR Type**
Enhancement


___

### **Description**
- Added Segment analytics tracking for key actions

- Implemented event tracking for project upgrades

- Added tracking for organization invites

- Included analytics for GitHub project connections

- Implemented tracking for new project creation


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>UpgradeProjectDialogContent.tsx</strong><dd><code>Add
Segment tracking for project upgrades</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/components/common/TransferOrUpgradeProjectDialog/UpgradeProjectDialogContent.tsx

<li>Imported useCurrentOrg and analytics<br> <li> Added Segment tracking
for 'Project Upgraded' event<br> <li> Included detailed project and
organization data in the event


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3313/files#diff-ced98d2b8b0e83e41fd9bd569a6dd3fb5c4013861d3352628e63abe0c285d2ba">+20/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>PendingInvites.tsx</strong><dd><code>Implement Segment
tracking for organization invites</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/components/members/components/PendingInvites/PendingInvites.tsx

<li>Imported analytics from Segment<br> <li> Added tracking for
'Organization Invite Sent' event<br> <li> Included organization and
invitee details in the event


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3313/files#diff-dec81af68d9403085c09185e0153335ddf7d629f64f626cae394108bcb42d685">+11/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>EditRepositorySettingsModal.tsx</strong><dd><code>Add
Segment tracking for GitHub project connections</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/git/common/components/EditRepositorySettingsModal/EditRepositorySettingsModal.tsx

<li>Imported analytics from Segment<br> <li> Added tracking for 'Project
Connected to GitHub' event<br> <li> Included project and repository
details in the event


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3313/files#diff-eb88f4f79aa0286c7f1d06ff73908f34009e7e9e8b982f54866f157fd81c5c3a">+12/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>new.tsx</strong><dd><code>Implement Segment tracking
for new project creation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/pages/orgs/[orgSlug]/projects/new.tsx

<li>Imported analytics from Segment<br> <li> Added tracking for 'Project
Created' event<br> <li> Included project, organization, and region
details in the event


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3313/files#diff-ef97470126e3edc146dda51337aaec556387e2f8a37afa70810d1dc94958f4fd">+10/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>hungry-terms-retire.md</strong><dd><code>Add changeset
for Segment analytics feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/hungry-terms-retire.md

<li>Added changeset file for minor version bump<br> <li> Described
feature addition of Segment events


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3313/files#diff-3accee3677ac6171593ed474c4c867ce1d27b490b69c9fd738f1cff121791ba9">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-28 11:14:51 +00:00
Russians tortured my 18yo friend Ivan bc of ukr flag in mobile phone
fad7f640de fix (examples/nextjs): typo (#3309)
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-04-28 09:58:08 +02:00
robertkasza
5ff4dd6e40 fix (packages): update storage/auth e2e config (#3306)
### **PR Type**
Enhancement, Tests


___

### **Description**
- Update e2e configuration for hasura-auth-js and hasura-storage-js

- Modify CI workflow for package-specific Nhost CLI shutdown

- Adjust test scripts in package.json files

- Add changeset for patch updates


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>fluffy-shoes-cross.md</strong><dd><code>Add changeset
for auth and storage package updates</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/fluffy-shoes-cross.md

<li>Add new changeset file for patch updates<br> <li> Specify changes
for @nhost/hasura-storage-js and @nhost/hasura-auth-js


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3306/files#diff-bfec01342bb3adca8f892b1223015496f20919e642eb0cd4f2fc42f4a659f93b">+6/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ci.yaml</strong><dd><code>Update CI workflow for
package-specific Nhost CLI shutdown</code></dd></summary>
<hr>

.github/workflows/ci.yaml

<li>Add new step to stop Nhost CLI for specific packages<br> <li> Ensure
Nhost CLI stops even if previous steps fail


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3306/files#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd">+6/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Modify test scripts for
hasura-auth-js package</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/hasura-auth-js/package.json

<li>Update ci:test script to use vite.config.e2e.json<br> <li> Remove
Nhost CLI shutdown from ci:test script


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3306/files#diff-69ad7e7b51fc8532cb472f319780be5355e41c386f1cc223ce929369c3b87500">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update test script for
hasura-storage-js package</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

packages/hasura-storage-js/package.json

- Remove Nhost CLI shutdown from ci:test script


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3306/files#diff-eca15b254298d1b63d7c80b470d31e046d63ae93b1f09eb6dc3959e3a326560d">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-25 18:21:09 +02:00
David BM
0bf28085b7 chore (dashboard CI): refresh hasura metadata before e2e tests (#3314) 2025-04-25 17:40:29 +02:00
Alexander Mart
b302dbd27d docs: add sveltekit quickstart (#3302)
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2025-04-24 14:04:48 +02:00
David BM
72a365c5fc fix (dashboard): correct graphql role dropdown source (#3291) 2025-04-21 18:19:09 +02:00
David Barroso
d11363a74c chore (observability): make alerts less sensitive (#3310)
### **PR Type**
Enhancement


___

### **Description**
- Increase alert sensitivity time from 5m to 15m

- Change NoData state to Alerting for most rules

- Modify execErrState to Alerting or OK

- Adjust noDataState for specific alert rules


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>rules_nhost.yaml</strong><dd><code>Adjust alert
sensitivity and error handling configurations</code></dd></summary>
<hr>

observability/grafana/rules_nhost.yaml

<li>Increased 'for' duration from 5m to 15m for multiple alerts<br> <li>
Changed 'noDataState' from NoData to Alerting for most rules<br> <li>
Modified 'execErrState' to Alerting or OK depending on the rule<br> <li>
Adjusted 'noDataState' for specific alert rules (e.g., OK to Alerting)


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3310/files#diff-27165812186176e21d13a35136e43511b837700a599d3a00c61a1f6b36c55af2">+13/-13</a>&nbsp;
</td>

</tr>
</table></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>
2025-04-20 13:59:31 +02:00
David BM
1bc2fabe59 chore (CI): skip CI runs on documentation change (#3307)
### **User description**
Skips CI running if we only changed under `docs/`


___

### **PR Type**
Enhancement


___

### **Description**
- Skip CI runs for changes in 'docs/' directory

- Update CI workflow configuration in GitHub Actions


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ci.yaml</strong><dd><code>Update CI workflow to ignore
documentation changes</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.github/workflows/ci.yaml

<li>Add 'docs/**' to paths-ignore for push and pull_request events<br>
<li> Prevent CI from running on documentation-only changes


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3307/files#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-16 15:35:41 +02:00
robertkasza
f8243f9434 chore (examples/svelte): update @sveltejs/kit (#3305)
### **PR Type**
Enhancement, Documentation


___

### **Description**
- Update @sveltejs/kit to version 2.20.6

- Add changeset for @nhost-examples/sveltekit patch

- Update package resolutions for security


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>neat-eggs-chew.md</strong><dd><code>Add changeset for
sveltekit example update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/neat-eggs-chew.md

<li>Add new changeset file for @nhost-examples/sveltekit<br> <li>
Specify patch update for the package<br> <li> Include description of the
change


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3305/files#diff-f07ba45e000aecdb0b0c45fbc4a50ccc0749e7755fdd86d630b9025cdde187ee">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update @sveltejs/kit
dependency version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/quickstarts/sveltekit/package.json

- Update @sveltejs/kit from 2.11.1 to 2.20.6


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3305/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add security resolution
for @sveltejs/kit</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

package.json

- Add resolution for @sveltejs/kit >= 2.20.6


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3305/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-16 12:48:40 +02:00
robertkasza
d9eb90604d fix: update vite and nextjs because of vulnerability (#3303)
### **PR Type**
Bug fix


___

### **Description**
- Update Vite and Next.js versions for security

- Add new version constraints for Vite and Next.js

- Create changeset for patch updates to multiple packages


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>neat-mugs-bake.md</strong><dd><code>Add changeset for
Vite and Next.js updates</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/neat-mugs-bake.md

<li>Add new changeset file for patch updates<br> <li> List affected
packages including dashboard and examples<br> <li> Describe fix as
updating Vite and Next.js


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3303/files#diff-6ec609b553758bab0278f440f07a0af9e742df25dbdc038b6212bb683f88eb59">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update dependency
version constraints</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

package.json

<li>Add new version constraint for Next.js (>=14.2.26)<br> <li> Update
Vite version constraints (>=5.4.18 and >=6.2.6)<br> <li> Remove outdated
Vite version constraint


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3303/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+4/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-16 09:17:12 +02:00
Nuno Pato
cef647194d fix: dashboard: add anonid to user's metadata (#3282)
### **PR Type**
Enhancement


___

### **Description**
- Add anonymous ID to user metadata during signup

- Integrate Segment analytics for anonymous ID retrieval

- Update GitHub sign-in to include anonymous ID

- Add changeset for version bump and changelog


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>signup.tsx</strong><dd><code>Integrate anonymous ID in
signup process</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/pages/signup.tsx

<li>Import Segment analytics<br> <li> Add state for anonymous ID<br>
<li> Fetch anonymous ID on component mount<br> <li> Include anonymous ID
in email and GitHub signup


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3282/files#diff-fc2b5989e3bbafda1d3d8b2317d24c39ef2b8cec0c4dc410170fa2da13464f68">+19/-1</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>tall-eggs-battle.md</strong><dd><code>Add changeset for
dashboard update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/tall-eggs-battle.md

<li>Add changeset file for version bump<br> <li> Describe change as
adding anonid to user's metadata


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3282/files#diff-26ba0d1f688299d031611809f726356bdec0104a9264ec7dcca0757985023a58">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-10 14:29:56 +00:00
robertkasza
efd68c3f92 chore (react-apollo): run e2e on preview instead of dev server (#3295)
### **PR Type**
Enhancement


___

### **Description**
- Run e2e tests on preview build instead of dev server

- Update Playwright configuration for better test reliability

- Add new script for building and previewing in one step

- Improve clean and install process with new script


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>playwright.config.ts</strong><dd><code>Update
Playwright config for preview build and improved
tracing</code></dd></summary>
<hr>

examples/react-apollo/playwright.config.ts

<li>Changed webServer command from 'pnpm dev' to 'pnpm
build:preview'<br> <li> Updated trace option from 'on-first-retry' to
'retain-on-failure'


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3295/files#diff-b22b77d29b0086af8ed742e35817880023261afccab0a6a41093e2a6d58715aa">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add build:preview script
and specify preview port</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/package.json

<li>Added port 3000 to preview script<br> <li> Introduced new
'build:preview' script combining build and preview


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3295/files#diff-d95dc3391741287366ea2e61f70e9ccc64452e0d22b1db91d6bf524f5aa4331c">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add clean:install script
for project maintenance</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

package.json

- Added new 'clean:install' script for cleaning and reinstalling


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3295/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-10 15:51:05 +02:00
robertkasza
233232b06f feat (dashboard): improve upgrade project (#3257)
### **PR Type**
Enhancement, Tests


___

### **Description**
- Introduced `TransferOrUpgradeProjectDialog` to unify transfer and
upgrade dialogs.

- Enhanced project upgrade flow with new components and logic.

- Added comprehensive tests for the new upgrade and transfer
functionalities.

- Replaced `TransferProjectDialog` with `TransferOrUpgradeProjectDialog`
across the codebase.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Miscellaneous</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>SelectOrgAndProject.tsx</strong><dd><code>Removed unused
import statement.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &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/3257/files#diff-7d86c6e5bc51696bf1aa421c920e01a1447699456c37b025bdc407050c7b5613">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>OverviewTopBar.tsx</strong><dd><code>Updated import for
`UpgradeProjectDialog`.</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/3257/files#diff-560ae107ed8e458fa4b4a226b9f5c24e24b042b5f9bcea9317c78e75929faa4b">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>16
files</summary><table>
<tr>
<td><strong>UpgradeToProBanner.tsx</strong><dd><code>Updated to use
`TransferOrUpgradeProjectDialog`.</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-f38fc14d24ec6ee22f9a100cc473c641dcdc66284d41d030c456bf505094ed9d">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>StripeEmbeddedForm.tsx</strong><dd><code>Wrapped
`EmbeddedCheckoutProvider` with a scrollable container.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-d8e63f9bdc9c2c672a4caabd406bf77bec4e4988e716d2b9e101182a863eb495">+10/-8</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>TransferProject.tsx</strong><dd><code>Replaced
<code>TransferProjectDialog</code> with
<code>TransferOrUpgradeProjectDialog</code>.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-bb5ac90e4fcb5841e3fef912beec1b1dbe83b273eea7a9e39fb258ff0361e7e3">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>FinishOrgCreationProcess.tsx</strong><dd><code>Refactored to
use <code>useFinishOrgCreation</code> hook for dynamic status
<br>handling.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-7602855e6aaab1dd3810c866acbedd5b9eb22c271806969eb9a3435f1c76ca8d">+13/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>FinishOrgCreation.tsx</strong><dd><code>Simplified
`FinishOrgCreation` component logic.</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-9e3ccc4f3c0168746e53b68211d07391593712d5d74847861248cfa7da31dd7d">+4/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>TransferOrUpgradeProjectDialog.tsx</strong><dd><code>Introduced
`TransferOrUpgradeProjectDialog` component.</code>&nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-06d6ae707f06c0db49a8930a8756195899ece09f08affa44aeadedce4b208948">+105/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>TransferProjectDialogContent.tsx</strong><dd><code>Added
`TransferProjectDialogContent` for transfer logic.</code>&nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-3f66f2e8af0175d1c3f9d4940b8dc965fefa18967c8f4977739ac73000708763">+100/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>TransferProjectForm.tsx</strong><dd><code>Added
`TransferProjectForm` for organization selection and
transfer.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-3324c79d8b4d48777467132ba0f13a95d4b0f1a9fbb4df9fd7f67735ac40cbbd">+186/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>UpgradeProjectDialogContent.tsx</strong><dd><code>Added
`UpgradeProjectDialogContent` for project upgrade flow.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-ced98d2b8b0e83e41fd9bd569a6dd3fb5c4013861d3352628e63abe0c285d2ba">+96/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Exported
`TransferOrUpgradeProjectDialog`.</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/3257/files#diff-bd61908ca8ab41f1a88cdcc3bafe4264b1e8120d7f65ff64f158631dd4e65a58">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>NotificationsTray.tsx</strong><dd><code>Added router
readiness check in `NotificationsTray`.</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-8b559ee1d3176203e8a4e1588924d57944d09d792117ed578b27cd5401ee5d4f">+3/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useFinishOrgCreation.ts</strong><dd><code>Added router
readiness check to `useFinishOrgCreation`.</code>&nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-3b8bf7608ab36d8ab0df895e400f0d2d9e29fad2055b40b33d8d9912a27c99c3">+1/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationPaused.tsx</strong><dd><code>Replaced
<code>TransferProjectDialog</code> with
<code>TransferOrUpgradeProjectDialog</code>.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-14afdf5ac20f058c26563a6992a3751f11cf173eec27206001262b5d1b3b979f">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>UpgradeNotification.tsx</strong><dd><code>Updated to use
`TransferOrUpgradeProjectDialog`.</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-f712e65a6e88f2731fc5597117f716594311087f8090e3e8f5f76e1a67c95188">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>UpgradeProjectDialog.tsx</strong><dd><code>Updated to use
`TransferOrUpgradeProjectDialog` for upgrades.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-7bfab4ad088dbc503c1304f5620e22e02f70602bf14ba6b495969b882b2eb30e">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>verify.tsx</strong><dd><code>Refactored to use
`FinishOrgCreationProcess` with hooks.</code>&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-5fa0ea2519bed6649a8aa98826526945868bd7a925c5ce5edb3fd14e81273947">+1/-5</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>TransferOrUpgradeProjectDialog.test.tsx</strong><dd><code>Added
tests for `TransferOrUpgradeProjectDialog`.</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-1b274953c536fcd901f72765ab134a34641442655988bde5595f63265a9e7ce9">+155/-12</a></td>

</tr>

<tr>
<td><strong>NotificationsTray.test.tsx</strong><dd><code>Added tests for
router readiness in `NotificationsTray`.</code>&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-727f6debec6a102557407e55c56363e0c75486e30a732158f85c81ada892f77c">+39/-4</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Cleanup</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>TransferProjectDialog.tsx</strong><dd><code>Removed
deprecated `TransferProjectDialog`.</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/3257/files#diff-b68d4641a67e07a8bf8c14e1f705059c564e1bca53e591783581af27a488d86e">+0/-306</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>index.ts</strong><dd><code>Removed export for deprecated
`TransferProjectDialog`.</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-ed023a2c08c77e3693789305cf9b9f2cd871090acf7b0775c7d7434903710c42">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>tame-planes-sleep.md</strong><dd><code>Added changeset for
project upgrade dialog improvements.</code>&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3257/files#diff-c83c4e28de9a00c1ee2cb4ad9867d2c42415c01c80e990205c351e6f5c8a6f83">+5/-0</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>
2025-04-10 15:15:25 +02:00
David Barroso
5e962300f6 fix (docs): fixing mintlify breaking our docs (#3297) 2025-04-10 13:23:31 +02:00
Nuno Pato
048b3389e6 chore: docs: add segment analytics (#3294)
### **PR Type**
Enhancement


___

### **Description**
- Added Segment analytics integration to documentation

- Configured Segment key in docs.json file


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>docs.json</strong><dd><code>Configure Segment analytics
in docs.json</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

docs/docs.json

<li>Added 'integrations' object with Segment configuration<br> <li>
Included Segment API key for analytics tracking


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3294/files#diff-873ce17c654718debe2fe308a2f2279bde8663686423c51f97fab2dd0722b8d9">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-08 12:17:05 +00:00
github-actions[bot]
be8cd6c3a6 chore: update versions (#3277)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-auth-js@2.11.0

### Minor Changes

-   d26b6b8: fix: update broadcasted session directly

## @nhost/apollo@8.0.7

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost/react-apollo@17.0.3

### Patch Changes

-   @nhost/apollo@8.0.7
-   @nhost/react@3.10.3

## @nhost/react-urql@14.0.3

### Patch Changes

-   @nhost/react@3.10.3

## @nhost/nextjs@2.2.6

### Patch Changes

-   @nhost/react@3.10.3

## @nhost/nhost-js@3.2.7

### Patch Changes

-   Updated dependencies [d26b6b8]
    -   @nhost/hasura-auth-js@2.11.0

## @nhost/react@3.10.3

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost/vue@2.9.4

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost/dashboard@2.27.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities
- 4fd176b: chore: re-add user event ci tests, updated sveltekit example
tests to e2e suite

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
- 0420e4f: fix (dashboard): Display the selected date's month in the
datetime picker component
    -   @nhost/react-apollo@17.0.3
    -   @nhost/nextjs@2.2.6

## @nhost/docs@2.30.0

### Minor Changes

-   38e7e9d: fix: remove community as it isn't ready

## @nhost-examples/codegen-react-apollo@0.7.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/react@3.10.3
    -   @nhost/react-apollo@17.0.3

## @nhost-examples/codegen-react-query@0.7.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/react@3.10.3

## @nhost-examples/codegen-react-urql@0.6.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/react@3.10.3
    -   @nhost/react-urql@14.0.3

## @nhost-examples/sveltekit@0.7.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities
- 4fd176b: chore: re-add user event ci tests, updated sveltekit example
tests to e2e suite

### Patch Changes

-   b89500d: fix: use nhost-js version from the workspace
-   a1333df: fix: update vite because of vulnerability
    -   @nhost/nhost-js@3.2.7

## @nhost-examples/react-apollo@1.5.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities
-   25f07a3: fix: update versions

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/react@3.10.3
    -   @nhost/react-apollo@17.0.3

## @nhost-examples/react-gqty@1.5.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/react@3.10.3

## @nhost-examples/vue-apollo@0.11.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/nhost-js@3.2.7
    -   @nhost/apollo@8.0.7
    -   @nhost/vue@2.9.4

## @nhost-examples/vue-quickstart@0.5.0

### Minor Changes

- 013e1c1: fix: update vite and image-size dependencies to address
security audit vulnerabilities

### Patch Changes

-   a1333df: fix: update vite because of vulnerability
    -   @nhost/apollo@8.0.7
    -   @nhost/vue@2.9.4

## @nhost-examples/cli@0.3.20

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost-examples/multi-tenant-one-to-many@2.2.21

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost-examples/nextjs@0.4.6

### Patch Changes

-   @nhost/react@3.10.3
-   @nhost/react-apollo@17.0.3
-   @nhost/nextjs@2.2.6

## @nhost-examples/node-storage@0.2.20

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost-examples/nextjs-server-components@0.5.5

### Patch Changes

-   @nhost/nhost-js@3.2.7

## @nhost-examples/react-native@0.1.7

### Patch Changes

-   @nhost/react@3.10.3
-   @nhost/react-apollo@17.0.3

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-08 13:52:27 +02:00
David Barroso
b89500d175 chore (workspaces): fixes to the workspace (#3287)
### **PR Type**
Enhancement, Bug fix


___

### **Description**
- Update @nhost/nhost-js dependency to use workspace version

- Modify pnpm-workspace.yaml to include more examples

- Exclude specific templates from workspace

- Add changeset for @nhost-examples/sveltekit patch


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>late-ghosts-taste.md</strong><dd><code>Add changeset
for SvelteKit example patch</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/late-ghosts-taste.md

<li>Add new changeset file for @nhost-examples/sveltekit<br> <li>
Specify patch update for using nhost-js from workspace


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3287/files#diff-1578fc8821e79f4e226a9d0f78fb415e5b6cbd0a71f41e4f15ec6b91f2cc4842">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update nhost-js
dependency to workspace version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/quickstarts/sveltekit/package.json

- Update @nhost/nhost-js dependency to use workspace version


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3287/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>pnpm-workspace.yaml</strong><dd><code>Refine workspace
package inclusions and exclusions</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pnpm-workspace.yaml

<li>Change 'examples/*' to 'examples/**' for broader inclusion<br> <li>
Exclude CRA and React Native templates from workspace


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3287/files#diff-18ae0a0fab29a7db7aded913fd05f30a2c8f6c104fadae86c9d217091709794c">+3/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>

Co-authored-by: robertkasza <167509084+robertkasza@users.noreply.github.com>
2025-04-08 12:13:59 +02:00
David BM
013e1c1d70 fix (ci lint): update vite and image-size dependencies to address security audit vulnerabilities (#3293)
### **User description**
Addresses advisories:
https://github.com/advisories/GHSA-m5qc-5hw7-8vg7
https://github.com/advisories/GHSA-xcj6-pq6g-qj4x


___

### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Update Vite to address security vulnerabilities

- Update image-size dependency for security

- Add changeset for version bumps

- Update package resolutions for security fixes


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>flat-suits-join.md</strong><dd><code>Add changeset for
version bumps</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-985f5074afc6182f003fda21514c3398427504e76a81e28b730920c5cf2b420e">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>10
files</summary><table>
<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-83675898dc6ed88838763232d022f6e100e07d71681cc8a1f02aee99ee3f229b">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-9fb3a23f389ab1d192d7e018d2acbe512bd8792278662101401caa98692735db">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-cb7094614884e8cd2c8fb67dadedb1887c46c31b888840def0b7042273bfbb28">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-d95dc3391741287366ea2e61f70e9ccc64452e0d22b1db91d6bf524f5aa4331c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-8a3e5ed0f618f15211c31f700e0da998e2eae58f60353624b7a7e637bd63b153">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-fc4298d3512fdd9a3d871f9f182fe871c8beccd1580f864a271ddfb32005feef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3293/files#diff-85166d1137e29a5275f991e1e94a0c9d5b83ac7504463ba76f9187b2b750c895">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite and image-size
dependencies</code>&nbsp; &nbsp; &nbsp; &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/3293/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+6/-6</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>

---------

Co-authored-by: robertkasza <167509084+robertkasza@users.noreply.github.com>
2025-04-08 09:45:49 +02:00
David BM
4fd176bce2 chore (ci): re-add user event tests (#3288)
### **PR Type**
Tests, Enhancement


___

### **Description**
- Reintroduce user event tests in CI

- Update SvelteKit example tests to e2e suite

- Refactor TestUserEvent class for improved testing

- Add new tests for database and backup features


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>7
files</summary><table>
<tr>
<td><strong>DateTimePicker.test.tsx</strong><dd><code>Add comprehensive
tests for DateTimePicker component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-c7076012eb33d6f60049710638b5ad19c2f310b8c250c79f1905be7e0a30b00a">+177/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>TimePicker.test.tsx</strong><dd><code>Update TimePicker
tests to use TestUserEvent</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-784f69003ebbc9e39837b920007cef14125a5fc48bb9114226820bcb2b0827b0">+6/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>TransferProjectDialog.test.tsx</strong><dd><code>Add tests
for TransferProjectDialog component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-d4ebdb8af76a7c9e73606708718c3448445545259ad553d73b6d322408e3eb8c">+234/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>ImportBackupTabContent.test.tsx</strong><dd><code>Add tests
for ImportBackupTabContent component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-753e5e6735a2d612b6ccc6617c053017ba591a763182fa28a8fc302731c3f347">+267/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>DatabasePiTRSettings.test.tsx</strong><dd><code>Add tests
for DatabasePiTRSettings component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-85d1f82a571b56469eab40dcc164fdd1e107fba79611ddd5cca7c191fe5117b4">+188/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>ResourcesForm.test.tsx</strong><dd><code>Update
ResourcesForm tests to use TestUserEvent</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-8828db70c080be6fc19f88059b08587584f1c23c9159092d6b186ca82a1943aa">+60/-55</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>resourceSettingsQuery.ts</strong><dd><code>Update
resourcesAvailableQuery mock data</code>&nbsp; &nbsp; &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/3288/files#diff-49b3a2a24ead48f97ace0b90f1ecaf4d4edbdef17109e29f5101016515e5946a">+12/-0</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>testUtils.tsx</strong><dd><code>Refactor TestUserEvent class
and remove utility functions</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-78f29250407edf853a353b48242d3cee59aa5724f38a60bb23bebdfc1ea2f9b5">+35/-18</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update SvelteKit example test
script to e2e</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/3288/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>nasty-cherries-cover.md</strong><dd><code>Add changeset for
user event CI tests</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3288/files#diff-653e520d91e00e8c62155076a5acfb2a606381f63c4c87b42ac70d23e7c97a01">+6/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td><strong>PointInTimeBackupInfo.test.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3288/files#diff-3980415ca79bf039abb469281fff9b1dc1de0a1ef52b4044d8c6f529538b6edf">+356/-0</a>&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>
2025-04-07 17:15:33 +02:00
David Barroso
d26b6b848d chore (auth-js): add missing changeset (#3286)
### **PR Type**
Enhancement


___

### **Description**
- Update broadcasted session directly in @nhost/hasura-auth-js


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>honest-countries-melt.md</strong><dd><code>Add
changeset for session broadcast update</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/honest-countries-melt.md

<li>Added changeset file for @nhost/hasura-auth-js<br> <li> Specified
minor version bump<br> <li> Described fix for updating broadcasted
session directly


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3286/files#diff-a9bbe8d89f97fb5a46896d2cf1e4fd059d55360261ce3bc37b34fd54bd246076">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-04 13:28:23 +02:00
David Barroso
3df7ca2a33 fix (hasura-auth-js): update broadcasted session directly (#3284)
### **PR Type**
Enhancement, Bug fix


___

### **Description**
- Update broadcasted session with full data

- Improve cross-tab synchronization in Hasura Auth JS

- Enhance session update mechanism for better reliability

- Fix potential issues with token comparison and updates


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>internal-client.ts</strong><dd><code>Improve cross-tab
session synchronization</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

packages/hasura-auth-js/src/internal-client.ts

<li>Changed 'broadcast_token' to 'broadcast_session'<br> <li> Updated
session data handling in message listener<br> <li> Implemented direct
SESSION_UPDATE event with full session data<br> <li> Added null check
for context in token comparison


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3284/files#diff-e726a23b4f62ae8d40f4bebf7cffd7a559ff64defe779d072e9f69cea360515c">+16/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>machine.ts</strong><dd><code>Enhance session
broadcasting with comprehensive data</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

packages/hasura-auth-js/src/machines/authentication/machine.ts

<li>Updated broadcastToken function to send full session data<br> <li>
Changed message type from 'broadcast_token' to 'broadcast_session'<br>
<li> Added accessToken, user, expiresAt, and expiresInSeconds to payload


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3284/files#diff-a8fdfee087ad5a72ea0a64667e2a0c7f25baa84eaaf73ebfee3f5a5a1b7584d1">+7/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-04 13:16:36 +02:00
David Barroso
38e7e9deee chore (docs): remove community as it isn't ready (#3280)
### **PR Type**
Documentation


___

### **Description**
- Remove community section from documentation

- Delete Code of Conduct and Getting Involved pages

- Update docs.json to reflect removed community content


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>friendly-chairs-argue.md</strong><dd><code>Add
changeset for community section removal</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/friendly-chairs-argue.md

<li>Add changeset file for minor version bump<br> <li> Include fix note
about removing community section


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3280/files#diff-95b418c3e266bd80dbf1fe2e94420838dfd0d0852ece649fc52b529e535cfff5">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>coc.mdx</strong><dd><code>Remove Code of Conduct
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

docs/community/coc.mdx

- Delete entire Code of Conduct page


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3280/files#diff-dc80329f722e73bc46ee76902de782b4f696a4be0224658cbbb0a70127cb7627">+0/-128</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>getting-involved.mdx</strong><dd><code>Remove Getting
Involved page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

docs/community/getting-involved.mdx

- Delete entire Getting Involved page


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3280/files#diff-4c15a93e9b63664bed77a875378d805efc8dc787bc0e1d6a6f413a376c5e6983">+0/-57</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>docs.json</strong><dd><code>Update docs.json to remove
community section</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/docs.json

- Remove "Community" tab and its associated pages


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3280/files#diff-873ce17c654718debe2fe308a2f2279bde8663686423c51f97fab2dd0722b8d9">+0/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-04 08:49:03 +02:00
David Barroso
25f07a3763 chore (examples/react-apollo): update versions (#3281)
### **PR Type**
Enhancement


___

### **Description**
- Update Hasura, Auth, Postgres, and Storage versions

- Upgrade Node.js version for functions to 22

- Bump @nhost-examples/react-apollo package version


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>spicy-sloths-cover.md</strong><dd><code>Add changeset
for React Apollo example version update</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/spicy-sloths-cover.md

<li>Add changeset for @nhost-examples/react-apollo minor version
bump<br> <li> Include fix note for version updates


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3281/files#diff-dc307886bb3f342aef6ff14bfdb2b7e3937648852a603b0a3ef9c485b903f689">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>nhost.toml</strong><dd><code>Update core component
versions in nhost.toml</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/nhost/nhost.toml

<li>Update Hasura version to v2.46.0-ce<br> <li> Upgrade Node.js version
for functions to 22<br> <li> Update Auth version to 0.38.0<br> <li>
Update Postgres version to 16.6-20250311-1<br> <li> Update Storage
version to 0.7.1


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3281/files#diff-268d6c8dddd6990d60d62c1c923955c4e0e7549a80f0f5856192f889378416a0">+6/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-04 08:48:46 +02:00
David BM
38c3db4a9e fix (ci): dashboard unittests (#3285)
### **PR Type**
Tests


___

### **Description**
- Remove userEvent-based tests

- Delete unused test files

- Update ResourcesForm test to use userEvent


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>DateTimePicker.test.tsx</strong><dd><code>Remove
DateTimePicker test file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/components/common/DateTimePicker/DateTimePicker.test.tsx

- Removed entire test file


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-c7076012eb33d6f60049710638b5ad19c2f310b8c250c79f1905be7e0a30b00a">+0/-178</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>

<summary><strong>TransferProjectDialog.test.tsx</strong><dd><code>Remove
TransferProjectDialog test file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/components/common/TransferProjectDialog/TransferProjectDialog.test.tsx

- Removed entire test file


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-d4ebdb8af76a7c9e73606708718c3448445545259ad553d73b6d322408e3eb8c">+0/-234</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>

<summary><strong>ImportBackupTabContent.test.tsx</strong><dd><code>Remove
ImportBackupTabContent test file</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/backups/components/ImportBackupTabContent/ImportBackupTabContent.test.tsx

- Removed entire test file


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-753e5e6735a2d612b6ccc6617c053017ba591a763182fa28a8fc302731c3f347">+0/-267</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>

<summary><strong>PointInTimeBackupInfo.test.tsx</strong><dd><code>Remove
PointInTimeBackupInfo test file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/backups/components/common/PointInTimeBackupInfo/PointInTimeBackupInfo.test.tsx

- Removed entire test file


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-3980415ca79bf039abb469281fff9b1dc1de0a1ef52b4044d8c6f529538b6edf">+0/-357</a>&nbsp;
</td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>DatabasePiTRSettings.test.tsx</strong><dd><code>Remove
DatabasePiTRSettings test file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/database/settings/components/DatabasePiTRSettings/DatabasePiTRSettings.test.tsx

- Removed entire test file


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-85d1f82a571b56469eab40dcc164fdd1e107fba79611ddd5cca7c191fe5117b4">+0/-189</a>&nbsp;
</td>

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ResourcesForm.test.tsx</strong><dd><code>Update
ResourcesForm tests to use userEvent</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>


dashboard/src/features/orgs/projects/resources/settings/components/ResourcesForm/ResourcesForm.test.tsx

<li>Imported userEvent from '@testing-library/user-event'<br> <li>
Updated tests to use userEvent instead of custom click functions<br>
<li> Moved vCPU and Memory ratio validation test


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3285/files#diff-8828db70c080be6fc19f88059b08587584f1c23c9159092d6b186ca82a1943aa">+52/-48</a>&nbsp;
</td>

</tr>
</table></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>
2025-04-03 16:43:00 +02:00
robertkasza
a1333df2a1 fix: update vite because of vulnerability (#3283)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Update Vite to address security vulnerability

- Upgrade dependencies in Vue examples

- Add 'type: module' to Vue quickstart package

- Update resolutions for Vite versions


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>stale-horses-run.md</strong><dd><code>Add changeset for
Vite vulnerability fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

.changeset/stale-horses-run.md

<li>Add new changeset file<br> <li> List affected packages for patch
update<br> <li> Describe fix for Vite vulnerability


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3283/files#diff-79e66d2654e3803067439855123d20d162193a019ecf68b5b45ee1d0e344949d">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update sass dependency
in Vue Apollo example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/vue-apollo/package.json

- Update sass dependency from 1.32.0 to 1.86.1


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3283/files#diff-fc4298d3512fdd9a3d871f9f182fe871c8beccd1580f864a271ddfb32005feef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update package
configuration and dependencies in Vue quickstart</code></dd></summary>
<hr>

examples/vue-quickstart/package.json

<li>Add "type": "module" to package.json<br> <li> Update @unocss/reset
from 0.33.5 to 66.1.0-beta.8<br> <li> Update unocss from 0.33.5 to
66.1.0-beta.8


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3283/files#diff-85166d1137e29a5275f991e1e94a0c9d5b83ac7504463ba76f9187b2b750c895">+3/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Bug fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add Vite version
resolutions to address vulnerabilities</code>&nbsp; &nbsp;
</dd></summary>
<hr>

package.json

- Add resolutions for Vite versions 5.4.16 and 6.2.4


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3283/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+3/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-02 17:23:26 +02:00
robertkasza
0420e4fda4 fix (dashboard): display the selected date's month in the datetime picker component (#3276)
### **PR Type**
Bug fix


___

### **Description**
- Fix datetime picker to display selected date's month

- Add defaultMonth prop to Calendar component

- Update changeset for @nhost/dashboard package


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Bug
fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>calendar.tsx</strong><dd><code>Set default month in
Calendar component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/components/ui/v3/calendar.tsx

- Added `defaultMonth={props.selected}` to Calendar component


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3276/files#diff-63f846015a12e66f0c36ec550d502d6d00a250959957652d14460807f1fbe68d">+1/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>stupid-sloths-poke.md</strong><dd><code>Add changeset
for dashboard package update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/stupid-sloths-poke.md

<li>Added changeset file for @nhost/dashboard package<br> <li> Described
fix for datetime picker component


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3276/files#diff-4f6398b11015521b2039034f4ae61cd1ac5870421f1a52ddbf11f209aa141230">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-04-01 14:42:08 +02:00
github-actions[bot]
97f6642c43 chore: update versions (#3256)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@2.26.0

### Minor Changes

-   7b9cdf1: chore: remove legacy workspaces
-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/codegen-react-apollo@0.6.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/codegen-react-query@0.6.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/codegen-react-urql@0.5.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/sveltekit@0.6.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/react-apollo@1.4.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/react-gqty@1.4.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/vue-apollo@0.10.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

## @nhost-examples/vue-quickstart@0.4.0

### Minor Changes

-   1c4f321: fix: update vite to fix audit vulnerabilities

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 09:50:28 +02:00
David Barroso
69c1ffa766 feat (docs): overhaul structure (#3254)
### **PR Type**
Enhancement, Documentation


___

### **Description**
- Restructured docs navigation and content

- Updated links and paths throughout docs

- Refreshed images and examples in guides

- Added new content for AI, Auth, and Run


___



### **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>15
files</summary><table>
<tr>
<td><strong>docs.json</strong><dd><code>Restructure navigation and add
new sections</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/3254/files#diff-873ce17c654718debe2fe308a2f2279bde8663686423c51f97fab2dd0722b8d9">+616/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>welcome.mdx</strong><dd><code>Add new welcome page with
getting started links</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-b35cb8e6a6201730c2d95103d1275186d72e727686bfd6470256c0c30137a761">+65/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Add new getting started
overview page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-8c9b35da559a5de5fe14ee078573e8d487453e26ed760c03ffd7f0ad476ca24d">+88/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Add new products overview
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-745a45fa3dbe67784dd921e50865c7ef33fdc6488cff1ccc75d9db524799d8b3">+81/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Add new platform overview
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-63ed954170e482e58b02938bcf8ab3c5b9b76b1a37b23b521cd88de2685ab566">+46/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Update AI overview with new
content and links</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e36c2139a3deb3ca81742e73df8ce981aa4502fcb3713832636088eda8f120fd">+10/-10</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Add new Auth overview
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-fcb8a858a73ee17bb801d63453716d58b940d7b1e51f48c5fb184e34971866f2">+49/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Update Database overview with
new content</code>&nbsp; &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/3254/files#diff-542ffbd4d75869cef7479dbc59a2c7c67272879b4f219488193794567b545351">+8/-9</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Update Run overview with new
content and links</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-ca49842af7e87c264e3ce8c19f4df657890fa0965cc188dbffafcd6ced1c526c">+11/-11</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>overview.mdx</strong><dd><code>Add new Cloud overview
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-32f53230fbf8b84f6a60dbf37568f8a4ea4bcab6f2e00e4357cd3b7f4c50cb55">+70/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>style.css</strong><dd><code>Add new styles for welcome
page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &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/3254/files#diff-4dde236d1a1b6f7a24be281ce9e8212368612d66a631fa592bfe18653f57c601">+80/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>echo.ts</strong><dd><code>Add echo function
example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-95b428813572cd2a2abcaf0c6e243622d757860c22f170c82126e5d2cbb269f0">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>email-confirm-change.tsx</strong><dd><code>Add email confirm
change template</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &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/3254/files#diff-34aea348d369ef146295ec5c36c6df0fde8262277b93b98d7d9f4633092dc195">+129/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>signin-passwordless.tsx</strong><dd><code>Add signin
passwordless email template</code>&nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-2d67959219d5979cf79921d4e8a86e16cceb46cd1e909a1783b68d27a85a0998">+127/-0</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>email-verify.tsx</strong><dd><code>Add email verify
template</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-139c6d5e04e5f6d2ce2c8e08a513a9830752fd9baff2aab415c9e34b0cee9918">+127/-0</a>&nbsp;
</td>

</tr>
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>2 files</summary><table>
<tr>
<td><strong>docker-compose.yaml</strong><dd><code>Update Docker Compose
configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&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/3254/files#diff-47a924f5ea105a2a42b2c421901d43cf1f834a94be0c2f2f868d29dd8990b060">+481/-174</a></td>

</tr>

<tr>
<td><strong>.env.example</strong><dd><code>Update environment variables
example</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-08ca7f9ad9d499c71a30703d1bb00c4c599646480cfcc311972bfaa654530c45">+13/-25</a>&nbsp;
</td>

</tr>

</table></details></td></tr><tr><td><strong>Documentation</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>README.md</strong><dd><code>Update Docker Compose example
README</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-34955300bc0ac9daa466c28e7aa59683b9b0c89e16344cf0544772acfb971b8f">+184/-23</a></td>

</tr>

<tr>
<td><strong>README.md</strong><dd><code>Update main README with new doc
links</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3254/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5">+14/-14</a>&nbsp;
</td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>101 files</summary><table>
<tr>
  <td><strong>CONTRIBUTING.md</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DEVELOPERS.md</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-bd017515eb79a7fb7569b1d15e8963ea380123d4fdf779978dd4b3ab7500fd10">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>README.md</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-c15729e6c35a283a4b0eda60a991303b6c36c03903ba42dbf832bb8d0daa1a1a">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AuthenticatedLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2d69ccffd267658f76d77a864cdece93fc222e08f6025955795fc6f4697f60e7">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>docs.json</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-ba8b2e40409d3782ac444d6c60d7f478772311cb211acd1c24c791937e47f1c6">+7/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SubscriptionPlan.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2a5f070869055286b669e382b18d656935752803b9a1ef13390ac028c2a48ac4">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SettingsLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-aa21cda513a125d8cefc5e7b5e1c755128aa904657350abf0ce1cde21e27ca75">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AllowedEmailSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f4b2730b26266319aa6e705012da5bd20774881bc473411bd8b1619bbd0646d1">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AllowedRedirectURLsSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-5c4c3714c99421265e1c35dd4300423407f758555eab0622d1f3bf12e7eb13ce">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AppleProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2e75c4eada80cf228714593e2cd315108b5d10ff7f20bd91e8bc884f571f6f85">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>BlockedEmailSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3a99b1db51b5654043151df4d77ad1ec369dd6d475e3261f80bb52e55dd81296">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ClientURLSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-fd60e1f63909e5cf5a57ca7cb9eb5c8577683b638e94185cc840ce8fc6ad0d39">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DisableNewUsersSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-60f6b3603e0467216d9633f4f92879a37416e202b18f0a4da0171332492fb6cf">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DisableSignUpsSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-13b3734c8b8aed2e159affbfc9997846e85e2096e739479c72a09e9101d31faf">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DiscordProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-640c83d25085fd13cac559d4b567e2b14f0ef77e003d3b0a6fd4c35b2b5177f9">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>EmailAndPasswordSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-b4c8b368defc138ebbf777af773d0a98d00f7130e4f795b0fd83cf934bbf9a4a">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>FacebookProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-5103b8085b43ba8f884429a70076ac8707a1510f06d62b5bf5bd08380ef4385c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>GitHubProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-438abe40d1c5ea84110c526038042a65d4c960a87f0371c23fc5d493350c5bd7">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>GoogleProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3c17bcfb21f6d2066f4727df5d059cfe871a5e1cf5efede5fcdf97d86ce17dbd">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>GravatarSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e6d30e32ab062fd6060282190a4b28d86cd7aaf1a08fe3090056759ea43cfc02">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>LinkedInProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-1efc0012f8c04ff4d54a29d20c0bc81422bcb5d689f4141c52179d7e8c054a7f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MFASettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-edb42fdfa300aae0bea103b9b4cc379e3d5c49ed00646a30673473660982904f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MagicLinkSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-d698ac461b405af3109f38cf74a81eb919193c28a62fa8abda7f62ba573a38e8">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SMSSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-beffd7762c6b4f12ba0edd9e524fe07c33062f5d8c12d3783ae2bb42e1380f64">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SpotifyProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-93ba8b83aa965db9730f5f4aef9da2db8279a924a3812fc9e6f880173fa4235c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>TwitchProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-0f30d19f5b40424b59a85450a33f870a1a3e7b844e36e2ff7a92ce6c35a441c1">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>WebAuthnSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-d34c84f3b66fa2b843540e1829d3c827e38c46d5e663b8dcad11dac964a34080">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>WorkOsProviderSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f0cd2c2e6badf59f882d564bf06617345cb4243bc699af4d02420ec2aefb166e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>PointInTimeBackupInfo.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-35be453f6605231bcee5b7f7f78564eb7aa2be723f5169509f9dddfe84477fe6">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DatabasePiTRSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-7a638c446af8419249770dc8da1ea522f950163b1d0045020927216c38db8cec">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>EnvironmentVariableSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-621bb42cb9fe0a763d30e738ab075af2784e8538e5ed7ac6ff1aa132d1a38042">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SystemEnvironmentVariableSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-b952daa2a34e49a14c5a471477fa2d50583091e420d88a3b941503b092d18e5c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>EditRepositoryAndBranchSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e19e36c0830816cdd4d73cd058b91295bbfcbe65c37c36fa9a87e9c1f2e3b7ef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>BaseDirectorySettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-50bcccdf949a19ce69fa86acdd63b5291fa2beaba07191a62c87d40ea5b94e88">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DeploymentBranchSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-d8fc80cc734f593c686f873536856bf9103efb1115ca865709bbeb7bd940895e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>GitConnectionSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-9e7a97afc3500aa2f4b28bdf4acb135925d92a6a595e16a3808b0b90ecc6be58">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>JWTSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-4bc7ce8b3f6e45940e5137c199d24b7a62cf3f804bf9c51b34a5f1168567ef25">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ContactPointsSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-50a024995bad7b420fd717104a1584009e9fa44c508889dd125155f33d99f48e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MetricsSMTPSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-db01779fdfdece601aa83a1a2c256e65514602344a8556afd5832d32e465bc65">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MetricsSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-957bb404fee8d18aa45af9e878837d311b69d9805ac16fe8d2c0e9d3b431e906">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>features.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-036778e07a1cdf33b7d90d8110f75338f8cd6870cc68bb75cff0c880318cd92d">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>frameworks.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3833ff9ca6b5f1020384672b9de38149de400c57beaeb65ea255475ba8ce7da3">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>PermissionVariableSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-ffbd1a0083e64318b68922362b6392090e24facc2a6476dc31e54e988a7599f6">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ResourcesFormFooter.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-035d56050da913a9ab98c730bb88b34c734149053674204b86bd798d79f81371">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ServiceResourcesFormFragment.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-9b9bf7e4f4e4dd34502e1b636c9f9aabbb20defe43595a79aa7e3f7d89750029">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>RoleSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-4cd13b62487b2de616d26d895ce4bb3afc7380abf9f3831ef2b949d073802a1f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ComputeFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-b5600bae05b535d54dc04b2a847b6402b10575efd87a0e7098796f0f9ae96d51">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>HealthCheckFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-9287c48c51c8a48a4b4aedcdc195cd9c8c79d3b3e2072765608081bc341f7fba">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ImageField.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-6f618d85f2ca9a85b2a754f45e7b7e7803317be7e5dfd0e05bbee87c5bd1f116">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>PortsFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-75c4254c31fe6addb187b5d122dd1fa171c1a8875c0152a6c1b2a05257c61d4c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>custom-domains.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e35b13396a4aa0b96e35dd7a0b1a27d188c0d45fe20cbda99e2fd59b83da5574">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>rate-limiting.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f31e44eb689a0e65e60f6eb2701f7adf283582fa5014c8727dc4922ecbd8c657">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>snippet-example.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-5fdce31e426151efa036ecdc78f6842c3bcd644a3d7658b1c753ee80c55d3cc8">+0/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>coc.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-dc80329f722e73bc46ee76902de782b4f696a4be0224658cbbb0a70127cb7627">+128/-0</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>getting-involved.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-4c15a93e9b63664bed77a875378d805efc8dc787bc0e1d6a6f413a376c5e6983">+57/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>commands.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2053eb5138f4c468b9aa94e6fd7302ad2f577839be107741f265ae1b2d9bfcaa">+0/-158</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>getting-started.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-05cc8d760dce63f257bee91e9c0293424a63e0ed210d26c7bca78bc3a3d5d763">+0/-87</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>overview.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-dfcca51e047037e649bbf76e68ab3aa9161a85c1bd25cf385acc5e764bea0cd3">+0/-32</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>nextjs.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-291e724f8e8aadb8d126a30590af172b9b82fe187407c83cfc0673d76efd1188">+3/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>react.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-fdf7ec55eee7c46bd8f83f8bf10066a136d21181bd6a04d513ae4c3bfaec1dc5">+7/-21</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>reactnative.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-bac475908ec022811c05fccec3d0eae805b25419b65a5d2537d70c606415d586">+14/-28</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>vue.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-1fee09fc68c4d33cea15dcb726c3e3671fdfbfc605a1751337f685f3cf851ca5">+3/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>nextjs.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-a5210d45e7d33a57d43078dbe2a2ccbf0667b157291fd92c3986092d7d33ab9c">+10/-11</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>react.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-6f5adda9f7b29d98c68cab6ec754c0bac501666a49dd635ee830789e2c812b68">+9/-9</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>vue.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f6c4215fa6909fd3accebe0691a7364d17befb8ef90da5a4aeaee83d598c0540">+9/-9</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>overview.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3ca431109466557ec1ba7fbd4cb01fa0ad6316e3a9a2fe9c4a849b2760cc7613">+0/-115</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>overview.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2a040923190b20dd4aa651b3cae8a7be263e7c5d014a71e9f27d628fa7404c08">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>getting-started.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f5ce74103419f39f4217dd75f3e89517779c94615558ae726ae1b4519328a939">+0/-35</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>diagrams.txt</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-5f90e86736e92ab8b695c8cad8bd1d65d2be49609da7e693957bad0e238e563e">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>overview.mermaidjs</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e68718f71eddf030085b739d48f6067bbb15f2421256ff27620d00f022b0c710">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>sequence.mermaid</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-cec810243251bf77934d8689d65f3e7d33f7632decf51e5eb7115b17042c11d9">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>introduction.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-0166b0574213b999964155797259928739a25e0a09d0442bdd14ff8307dcca30">+0/-85</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>mint.json</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-c91a604899dfef4b2494c317f4fd39a7f22b79986095f580399347293d534deb">+0/-560</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>package.json</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-adfa337ce44dc2902621da20152a048dac41878cf3716dfc4cc56d03aa212a56">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>configuration-overlays.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-f94c8434a3810529922d6b73ebd5f348e4f978b973e2959de0c0e45889b914d9">+7/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>local-development.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-29e98b0241b9335e8929e64440664e4be275f3a3965a88d22a3eb80b5034fca1">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>migrate-config.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-0e2b5b8948935d313421790d173d10bb5c1242166f97d87fbf35d2a010d643f1">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>multiple-projects.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-b756a0fa80d9cdbefb538f676c90521bcff5e730498cde0430bfe789475c4e2f">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>overview.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-18be817b7d51e5caba56718075ae087777f3e3811987257d48949ada0fe96da8">+46/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>seeds.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-94215e4f4b5a3df8c20f0102f5755ecb55b5155d6ddbae30844666e477b496ab">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>subdomain.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3300c8ab184167028e82d2e66932958ac55ebe94fba7d1aa2e45e8180178ea0e">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>billing.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-5a3b0998ad9e2f09c66255dff3651de3c6d7999e8e9c84169481e5e8af95935d">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>compute-resources.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-e2c40097d219e905b83f9e5ea40b19f6b927f846d7834b37a2dbe93dea3f5299">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>custom-domains.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-6004c16e12e623eda4d50a3b2e4f8ecbf5c2e1bbc9e5a91a62232e3a35f76a9d">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>environment-variables.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-49e8324f160e8fbe2cfea76adb45de7bbc3da6ca3d64d3a786a0f188c8bec9dc">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>git.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-d427ee2887e5cae303ce21a0b08300a82955c0f7f231ecea2ee198b69b0feb8d">+52/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>logs.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-d04ffc497f0eac2496c6bb7d4dfbb49853f35ba380dd45baf2d3239f8a42d569">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>metrics.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-14a13543059bf5d4903769ef3e6a90bb63af5cacc6105a35689ff75936421ea2">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>rate-limits.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-16b5125772f57a495889904938e950431b7b03a886e32241a162554b952db0c1">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>secrets.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-4c49bd272574c88f0d475d349c782c4ea24ba5cef97127106ebddde5befe7f4e">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>service-replicas.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-9188cea8fe3017f7732d5c9ed6600ef0147da8e960cff24ac4c6e156b53c4be1">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>subdomain.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-6d4add19495d7514bbaf0d67d9e60cf83b10ca4de6012aaf39dc0e39f3086bc6">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>tls.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-ca2d4657dffa3ca239e06eef76855154feb9b155317c67ff182d79f81aeaa236">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>deployments.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-6fa07c021c9566a75e9aca5efbf0f4708bd9862bb5484bf95cc25ce00f30e853">+0/-38</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>community.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-4f14893ed8d4ebde9be284bec0c27adc760452f8939246a6a65361bfd70760b8">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>dedicated.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-78f989a9a039240f69408cb775632f7494754276051eae98af420dadf096c8cd">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>overview.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-7cc9f04b4559ab62c5104791051ff7d7ad8dea108f1720eb260aa8e68475b0ee">+40/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>support.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2aa4bcb1194ac3857cbb1846e43db4f1a5ebd0d9302e02308ce1502fb8c0a763">+17/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>product.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-3a7c615149cb270f3f59493a817306f87f8771114d1272de085359996e64bef2">+0/-56</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>authentication.mdx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-1d91de2bc59159b3d47e86967f6ea82c608a10eb277d3fc0b5734f6f19305089">+0/-66</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>Additional files not shown</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3254/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>

---------

Co-authored-by: Sumit Saurabh <62152915+sumitsaurabh927@users.noreply.github.com>
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2025-04-01 09:34:27 +02:00
David BM
8ea263ec75 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>
2025-03-27 16:25:39 +01:00
David BM
7b9cdf1f5f chore (dashboard): remove unused legacy workspaces code (#3209)
### **PR Type**
Enhancement, Bug fix


___

### **Description**
- Removed legacy workspace-related code and dependencies.

- Refactored and relocated utility functions and hooks for better
organization.

- Added new GraphQL enum value `Pitr` for billing report resource types.

- Improved type safety and validation in service resource forms.


___



### **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>13
files</summary><table>
<tr>
<td><strong>graphql.ts</strong><dd><code>Added `Pitr` enum and
refactored GraphQL queries.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-fbd5db84b560b1c91675004448c6c7fa0dcbfb28b9eb05d53b03e6cb7b83ebac">+177/-175</a></td>

</tr>

<tr>
<td><strong>ServiceResourcesFormFragment.tsx</strong><dd><code>Improved
type safety and validation for service resources.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9b9bf7e4f4e4dd34502e1b636c9f9aabbb20defe43595a79aa7e3f7d89750029">+46/-12</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>getPreviousApplicationState.ts</strong><dd><code>Added
utility to determine previous application state.</code>&nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-cdc095ecffb5d21ca54d8537f3d2359bb64c5a0ade4ee94ae9d842ea7342f3f0">[link]</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>ServiceForm.tsx</strong><dd><code>Updated imports and logic
for service form components.</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a02746694d45a84390d09b49a1b3eec85c25a8bd9a70b4834ee5af1ba82cb88e">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useProjectRoutes.tsx</strong><dd><code>Refactored project
routes hook to remove workspace dependency.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-ffb341175a52f91f88ce6906c93ff747944ffd3ed9ff9ed27f0894e88e778b66">+5/-7</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>ResourcesConfirmationDialog.tsx</strong><dd><code>Simplified
resource confirmation dialog logic.</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-31f0a9eb5e1c1199ad462b7e1a9886cf4941676dc2506661c3d304aa5cf1716a">+6/-8</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>

<td><strong>usePreviousApplicationStates.ts</strong><dd><code>Refactored
hook to use updated project state logic.</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-e0bdede95ef9307a61f227b0f6a7dd3be67470e4a58ff139bb5b569f5bef2680">+6/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>AuthenticatedLayout.tsx</strong><dd><code>Updated layout to
use refactored hooks.</code>&nbsp; &nbsp; &nbsp; &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/3209/files#diff-2d69ccffd267658f76d77a864cdece93fc222e08f6025955795fc6f4697f60e7">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>PortsFormSection.tsx</strong><dd><code>Updated imports and
types for ports form section.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-75c4254c31fe6addb187b5d122dd1fa171c1a8875c0152a6c1b2a05257c61d4c">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationUnknown.tsx</strong><dd><code>Updated imports for
application unknown component.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-d1d7044dd66488c5bc787a89612754b283eedb404d4d6abcface2fa533d5c9d3">+4/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ApplicationErrored.tsx</strong><dd><code>Updated imports and
logic for application errored component.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-77c5a4128ffd614f299c867e5e3508430946f8f40d4ef5825f57874371fb1101">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ServicesList.tsx</strong><dd><code>Updated imports and types
for services list.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-643c818a248c42950336289392ac97ed9ef5c670ff8e47b80588b9802844d28a">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>useHostName.ts</strong><dd><code>Added new hook to retrieve
host name.</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &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/3209/files#diff-89b0d30fdcc12b0b3ea97e76676101c2a535ec0817ef106e26f74736d190d1b0">[link]</a>&nbsp;
&nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Bug
fix</strong></td><td><details><summary>7 files</summary><table>
<tr>
<td><strong>OrgsComboBox.tsx</strong><dd><code>Removed workspace-related
logic from organization combo box.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0736dac185f4ed134d5b53be292c9a2ee4f6df65e965b801a2dbbc8a184b3687">+3/-57</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>Header.tsx</strong><dd><code>Simplified logic for opening
Dev Assistant.</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/3209/files#diff-edac1cd4478dc0ad12911ea2e486f40e49f6dc64eaf8e72084225d1f4e8725af">+6/-18</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useNotFoundRedirect.ts</strong><dd><code>Removed
workspace-related checks in not-found redirect logic.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-262687fd80c4510f966a57885b1cc42a6297fd89ab49f6ff49b0df59670027f1">+1/-14</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>index.tsx</strong><dd><code>Simplified navigation logic by
removing workspace handling.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4eefa54204aa396da4d4d2f1d633d42d1b8ef86987f6e8c9b63d81df1ea6a273">+3/-21</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>run-one-click-install.tsx</strong><dd><code>Removed
workspace-related project filtering.</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-00e84c02bfc3c34019e15f820b23e332eeb1933a745be330c3644cb0f63c92b5">+3/-17</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>useNavigationVisible.ts</strong><dd><code>Updated navigation
visibility logic to remove workspace dependency.</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-3a395e5461f6112ecf12f399ef008999133a1b3a9d9b267b2ea7f7d5d39d1fe0">+8/-8</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ErrorToast.tsx</strong><dd><code>Updated error toast to use
project context.</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/3209/files#diff-189ba99303a20e964b5e3f3d6f1cf95c6376780a59604d1dee98aa84d9a2a9dc">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>101 files</summary><table>
<tr>
  <td><strong>brave-fishes-tap.md</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-d01ffdb9103e911bc83d52df72b16006ca7d8af6c0ac8ca5504f022c0c3cbd0b">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>package.json</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>InviteNotification.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9209cf2ec7253c2a3ea03496f2e213b9f6ebf569264394ccd4c5cf5deef1f0b5">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>TimePickerInput.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-970eb8f755f27a1e13f0d24230c403ffd1e5ad7829e14b67690373bcdade1277">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGrid.test.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9cec5ba99b1f62307ecdd1444d14288bef737bac6d3eff1c02ddc2f89e9f9061">+0/-67</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>DataGrid.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-28be1550256357ac5d3668065e1e3a496e6896ac12121dba4dd0c98948c3d2f6">+0/-185</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9458085c38927aa3e04af020d769fc21a2b9b38dd190da9258a557d720c20f7c">+0/-4</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>useDataGrid.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0af098dd11eddfb502643d4a17599d3f96f70dda9d0dd76be58eb651e6ab3e08">+0/-110</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>DataGridBody.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-bd9e62c9a14fac478b95684e09943ad95493e3cd0f517d3b2bcf27d90a8b6b7c">+0/-315</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-095354baa6f9bcf08084323308fb51d62cdf1993fa2922f5b6008173654ff94c">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridBooleanCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-912f222369372de8803ab4ea2ab8310d2ea12f98f08e9e4ae76ce90b99e9e718">+0/-121</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-6aaf85dd782c1bd3f87baa67daa100ae57b06680b536ae261be81a9ae9f3225f">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-30b7b50f3aaa8f1da1709951acc8338ca283830474e9d701e5a4650540ea6f8a">+0/-381</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>DataGridCellProvider.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-c996a77e36cee151e515fd26d540a3e661c76e41f1cceb7de6f52bd51d5849b5">+0/-238</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-085c66def9eb67fc7c2a44f3421206a58c4ec60127e463251a2632061fbe8ea8">+0/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>useDataGridCell.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-cb0b09edfdc285173765bcbb7c4822083286dee9be4de07fbafe802a72d997c2">+0/-10</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridConfigContext.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-f6e013fae040f331dba8a8cfd69ecd9629e609299ca2613de378deef3e2b0a70">+0/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridConfigProvider.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a285c2ab50379b3e50ae912427b3bbf1748534fb688c23e0e9b3e9d63e3670a5">+0/-16</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-b495c649d04122385810916ab7e9cea8a36b3a2c636c376b44172a70804cf7a5">+0/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>useDataGridConfig.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-1ad1e4d3198ad963ac96fecbdd83f4061ed3133cf16ace8f7f936bf7721365ab">+0/-15</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridDateCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-fff3b2aebb93d3d3a351d7d4ccbc43d376b09ec623c1535c94509f4f35ae2da4">+0/-166</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-fbbdbffeef2e43799d1108293e0e37d7f2949540b4c9ab542beb340a6a84c115">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridFrame.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-8d8bcaeea226957a5ef1a58038b02c161950da8a2f7af5ee95c657775a204705">+0/-29</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-b4c9a950b5dcf474d3d8110544e8a049ff2c0d496aced6c82f7fd77c438c7e67">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridHeader.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-02b826079155107d8958b4c673ab2b16853a96e46ef6f57cd832eadfdc2092c8">+0/-233</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4fd4f6e3ee3ef5c4821bd8fa47ac59c2c6a8ca90dde1765ca8dde1a0689474f8">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridNumericCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-e43c9c46c38fcdb978debf6164c8961acb5205ee6d152176b63d8dd2af413aa0">+0/-110</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4497697c15accb04a0115741740c32a7bdef1590b395f3158b1099a627d501ce">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridPagination.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-70ecfb728f6db3744bb35fcf537f70b20fea7688a00d302db0c624ffc0a043f0">+0/-91</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-eb674985acf19a5b7e619edd684c7760a617dc50fde1e9d746b1e261e092ea97">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridPreviewCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a4b0178eb28a1feaec9a72ad10c12283920cebbbfb0191c60b2535cb9ae028c6">+0/-410</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-845756895046979c2172565ff6d7e02340b93ac9e4499f930df626a0f5097880">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DataGridTextCell.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-74741563ec9d20b8894db4b3e653d1b345de156cd7dea1aded30ae9f78c3a7d3">+0/-243</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-53ad1b029b8643989c03e80ec5bb988c9eea2cf0ca595df2bd9eeb173f035f9f">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AILayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-06c432465b00d94c1f3fc8021951bb4d25f4e38195a6b1d90bb728f9105ae632">+0/-46</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-d97b390020ef14021a1ae3e1643b3a4f329b13a8a68aa5353a8a3ab64e824f91">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AISidebar.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-29134460a234b6ae943e6c6d8d65ac8f9d66bf4e3403b045b94662b0f57a964a">+0/-143</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-b3af150842887ad9d9107eb9c70330ab8ae5b4b04932a1c3f671547c4e99301c">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>Breadcrumbs.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-faf66535b1957a4f15804af0dd708fd0a64847ae31d782db949ce0c6597e3683">+0/-78</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2cad195ac8af2b5acd41032c9ecbec8b476c9c348f08aa305ef0a3e81098a29d">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DesktopNav.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-547854a882b4681187d3e8beae5d8512fb3c49cef3017ac891f57b4f04e87fc2">+0/-90</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2f102e83204acf8d7bb8133d7366e65a168c77e212fbade105f67845d53e4622">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>BreadcrumbComboBox.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-1d14ffa8e3a2bbaf9d8bfd4f215670cc62ce74558420619b98dd563a82f5ddb2">+0/-109</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>MainNav.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-8a552e1cae4ec4725740e006ec406aa60057db39c9580a31d938709d17d4b2c3">+0/-12</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>PinnedMainNav.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0fbc67c16a16e263b51e46ada3fbaccc041074f31f541bf663ae3b4b5f2a2a17">+0/-12</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>TreeNavStateContext.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a3558f36294f2b09b28b3a7704b443f2f2c92c33a0d376f986ca4b365907e7a9">+0/-13</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>WorkspacesNavTree.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-f16c5269315ce432f3fadd7e5b6c67b5677c2565e86d8e992d7336c139240423">+0/-495</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>MobileNav.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-88408885daaec8805bd085b53462c9f2d95db32f7e523912837a8167211b4fb2">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>OrgProjectLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a123a6ce5d250edcfad8d67346a1e7ee5bb31f2416f8434c406f8c08e6d1c810">+0/-123</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-18c0d591ef16e8caecd0f371f139647bc68a958f300296cd5077e91f5a5efa73">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>ProjectLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-8caf89cbfa2eaba06d159548c1a77cd21b0da6553922bb5198bf8fb0f1d15512">+0/-114</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-8514a657bfc9d16c0c3d0cfc7df589876c15389cb9937fc15e46944e625413e6">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SettingsLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-5d2869e956e78a19f2c099eb43ed3edca826c599ea327e790ec09f2c07f92026">+0/-69</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-e0760e0de853e7e172f98c8c2932124d9a6bdd644d1751dffefaa39a13f313a3">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>SettingsSidebar.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-228be719ea3624edbfd2af99af3c076cebb3d0732026987306aa1032a795ba00">+0/-266</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-398edb964d1a53e31ed4afedda4f85455f37180b8bfa3b4164d848e9f8e25438">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>UnauthenticatedLayout.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-54ee5ad9c01e99ffb05218020a6b97d091cd97cc53ad27e950480a3e675f2220">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>StateBadge.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0785b6d6a702605f6410d8c76a303ed8bcaf33449f12b0cf6324f0250e94562d">+0/-58</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-d9c38a018643979391b0c0baea0e859dfb19eba0f06dcaa7198a789dcab24df8">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>spinner.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0404df81ce8503d881cd2f3f1ac59865aafd65f559e6a335cd79e2c49fe31476">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AssistantForm.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-f6015289d06f2d4d6b495549c0c54db2fb5833bac4ea659877b3e2c32668c758">+0/-309</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>ArgumentsFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-c0cbe9d5ad0a46c4cd994015d5b27452b484182bc59f9524e585389c784c6fad">+0/-165</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9a27cce2e15cc8f73013ea09de29d18b077b3dca6b70cbbb6f65868cc6e4f35a">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>GraphqlDataSourcesFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4a435a4417301fe788a7556a19e81b78fce505dc1c5fa6dcd7866ab4882e16b0">+0/-123</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a47e5e39caca5b4d48ebb44351bf53a131d3bbda63842702998eaf10758457bd">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>WebhooksDataSourcesFormSection.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-351156304a9a0e733ed3021f8403d66e0c90d982f0473ecdde18fbdb7cda62b6">+0/-123</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-ba756f916fe87414836dede20a92c9d6b92dd45e637a3b4997dda4a53afb6561">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-7a8d3a1c1699597e56716689080a27d87fa8e324b8d9630e376f6b7ac4e7b103">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AssistantsList.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-1a59975c1b045c82661af32381c5ba707d6148d3cbf32d2058cd1608a498bcf7">+0/-158</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-8cc30611ef5cd09068b1b6e72dec22807667298bb00b3d03dd857ac4e18ae693">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AutoEmbeddingsForm.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-652aaf2408d7a565c133280378b00f1f915b95f6e11a02c3284b3de1e3f0563e">+0/-333</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9b82349addb66b827deffab33b2278cc9d1592d73a7f3b5403388eb361f59e26">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>AutoEmbeddingsList.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2d5ad51ee4b313417bf25812aea7a7b2194d8b36ec7b21f2715c591f3a650979">+0/-172</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-071889af96589df81d86bd8f56518ac55414b56710c12e04422259f8f28417b1">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DeleteAssistantModal.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-936ee6ffc853afdfbe613d8f0691b0717b893867ef68a515772c1d1c2b34dbe2">+0/-101</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-416db234a1b6459a1aa10d159372597b06b57bd97bed9517447c328969cffcb7">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DeleteAutoEmbeddingsModal.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-cc09a2e76a36bd3d3a72662fc74dcd8567c72a60f65babe0f9d4cd24dc1049f5">+0/-126</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-3c86ad5feb48d5736588598c0d4f6f2a2e96316f2088db653cd3008694410580">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>DevAssistant.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-a70be4ce82cafa2a5cd0c587a5c927c1e21ebfe9aeebe0db45c91a7fdbfad216">+0/-235</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>LoadingAssistantMessage.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-7d85aea7bef56f2cd60bedcf0bceeea2d0a286ffaf41dae46e1a09ee3517d339">+0/-28</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-52e76ab9735b31f4a23067fb232087a07f366785bd9ed770a405cb53d7814ac6">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MessageBox.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4e2c9825294e6e1d27a82ca6275724223a438975b4c27bd2c4285c4b44192562">+0/-98</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-51ba9e75ccb25006d10dbdd1dc837f717ad35405a3f1cb7268e7e9c3efc8a0f5">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>MessagesList.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-ce3eee6db2c6205688e9e18e14bdf69ac086a9bb14b9ca700d363255bfd6285d">+0/-36</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0c1c5112ef0ff550e50083472c4664cd65ba358e40488475e6dfde20513c72b5">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-3ed20f43facb55a7acaaf127c3ec63647b04b6b035be34ba61407b0f361099ae">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-4d7e6c292d1217979f4119a3e981807d6bbd9a106d1e2e4c5129df2cdc38b9bf">+0/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>messages.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-d87f7975332092d0bb875f789ce3e06fef19d675a540805c520bb1d796fe2efc">+0/-23</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>projectMessages.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-29ee83e06b8c27fa0b56d3827e82241c59441b33107805c133e44866331aed4a">+0/-20</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>session.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-9f3f7f1876efbbff1133c150dd39505154d8d1e9d0d123e199f251258ad3a178">+0/-10</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>AISettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-caaa010fadfdc403e470035e2dbbd950c163d59f8192bfbccbf0d1dd159a57a5">+0/-496</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>DisableAIServiceConfirmationDialog.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-be999162874fbadbc93ca1bcabd881931c2673863e36c2e7b427f11fb7908879">+0/-110</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2fdab42f10d8c17e14864ea2ee736126e009985998cf4ec07a45f22d34deee82">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-c44601df3a538664a1d7bfbf27552fe36246ac69f27d4c35c9ede34f624a222a">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>getAISettings.gql</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-3bf40707417defbba9ec9de149f7f322ad11869ea89cb136b5b4fcffe74f3610">+0/-24</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-7e62ae21154e773aa7527bea511ff1a517139bade8f36feb177a0da185778a50">+0/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>isPostgresVersionValidForAI.test.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-b3649eb91d332adb390bf040e9eef975190a5080e6792076449a38296c9741ac">+0/-22</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>isPostgresVersionValidForAI.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-0f2b72f23874eaccaed79c80ffa750fdf0094940adfa733e8961322556195bfe">+0/-18</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>AllowedEmailSettings.tsx</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-33d6bf857db317c4533a74e8cfc36642892a30510f04425aa20a4f9e858814c3">+0/-197</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>index.ts</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-47c1d040f85882e659b4c936649cecf98ff50a5d87a075bba66250d9a84e6d89">+0/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>Additional files not shown</strong></td>
<td><a
href="https://github.com/nhost/nhost/pull/3209/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-03-27 14:56:39 +01:00
David BM
1c4f321f64 fix (ci): update vite to fix vulnerabilities audit (#3255)
Updated Vite minor versions across projects to address security advisory
vulnerability
https://github.com/advisories/GHSA-x574-m823-4x7w

### **PR Type**
Enhancement, Other


___

### **Description**
- Updated `vite` dependency across multiple projects to address
vulnerabilities.

- Added a changeset file documenting the `vite` update.

- Incremented minor versions for affected packages in the changeset.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Other</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>little-peaches-bathe.md</strong><dd><code>Added changeset
for `vite` update and version bumps</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3255/files#diff-1517254487ebcd9cd74441ddbed15984d623cbb85119925248e57242614a47d5">+13/-0</a>&nbsp;
&nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>10
files</summary><table>
<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-83675898dc6ed88838763232d022f6e100e07d71681cc8a1f02aee99ee3f229b">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-9fb3a23f389ab1d192d7e018d2acbe512bd8792278662101401caa98692735db">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-cb7094614884e8cd2c8fb67dadedb1887c46c31b888840def0b7042273bfbb28">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 6.0.12</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/3255/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-d95dc3391741287366ea2e61f70e9ccc64452e0d22b1db91d6bf524f5aa4331c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-8a3e5ed0f618f15211c31f700e0da998e2eae58f60353624b7a7e637bd63b153">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-fc4298d3512fdd9a3d871f9f182fe871c8beccd1580f864a271ddfb32005feef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-85166d1137e29a5275f991e1e94a0c9d5b83ac7504463ba76f9187b2b750c895">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Updated `vite` dependency to
version 5.4.15</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/3255/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+1/-1</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>
2025-03-26 13:15:04 +01:00
github-actions[bot]
60d4d28627 chore: update versions (#3239)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@2.25.0

### Minor Changes

- 34fdcb8: chore: add prettier plugins as devDependencies to root of
monorepo
- 4937c5e: fix: stop content overflowing in projects and database
permissions page
- 1542132: fix: update babel dependencies to address security audit
vulnerabilities

### Patch Changes

- 78436ca: chore (dashboard): add tests and small updates to PiTR
settings and restore page
- b5a3895: chore (dashboard): update page context after each navigation
-   9b24807: chore: fix link to PiTR documentation
-   ea65846: chore (dashboard): update nextjs to fix middleware exploit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-25 08:39:49 +01:00
David BM
34fdcb8863 chore (changeset): add dashboard prettier plugins to root of monorepo as devDependencies (#3252)
### **PR Type**
Enhancement


___

### **Description**
- Add Prettier plugins to root monorepo as devDependencies

- Include Prettier plugin for organizing imports

- Add Prettier plugin for Tailwind CSS

- Update changeset for @nhost/dashboard minor version bump


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>angry-rabbits-fly.md</strong><dd><code>Add changeset
for Prettier plugins addition</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/angry-rabbits-fly.md

<li>Add new changeset file for @nhost/dashboard<br> <li> Specify minor
version bump<br> <li> Describe addition of Prettier plugins as
devDependencies


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3252/files#diff-1dc91a69351de73036aee86088b5553c604a0b7b726d1134bc679c71e288eea8">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Add Prettier plugins to
package.json</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

package.json

<li>Add prettier-plugin-organize-imports as devDependency<br> <li> Add
prettier-plugin-tailwindcss as devDependency


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3252/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-03-24 19:44:17 +01:00
robertkasza
78436ca29e chore (dashboard): update PiTR (#3247)
- add tests
- add price to PiTR settings
- update link in PiTR settings
- add note with recommendation about restore
- add link to PiTR docs to restore page
- small bug fixes
2025-03-24 15:53:34 +01:00
robertkasza
ea6584614b chore (dashboard): update nextjs to fix middleware exploit (#3251)
### **User description**
More info:
https://zeropath.com/blog/nextjs-middleware-cve-2025-29927-auth-bypass


___

### **PR Type**
Enhancement


___

### **Description**
- Update Next.js to version 14.2.25

- Address middleware exploit vulnerability

- Improve dashboard security


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>nervous-shirts-rush.md</strong><dd><code>Add changeset
for Next.js update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/nervous-shirts-rush.md

<li>Add new changeset file<br> <li> Specify patch update for
'@nhost/dashboard'<br> <li> Include description of Next.js update


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3251/files#diff-f9ef884a817466b3e2f2e10938fd046e15c764241ea5a8b841e0fea8cb2242e9">+5/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Upgrade Next.js to
version 14.2.25</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/package.json

- Update Next.js dependency from 14.2.22 to 14.2.25


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3251/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></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>
2025-03-24 10:35:04 +01:00
David BM
4937c5e055 fix (dashboard): stop content overflowing in projects and database permissions page (#3240) 2025-03-21 12:05:39 +01:00
robertkasza
b5a3895e16 chore (dashboard): update page context after each navigation (#3248)
### **PR Type**
Tests


___

### **Description**
- Added `updatePageContext` utility function for consistent page context
updates.

- Refactored e2e tests to use `updatePageContext` and `gotoAuthURL`.

- Improved test setup by centralizing navigation logic.

- Enhanced maintainability of e2e tests with reusable utilities.


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>13
files</summary><table>
<tr>
<td><strong>manage-pat.test.ts</strong><dd><code>Integrated
`updatePageContext` in PAT management tests</code>&nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-891790fa0d9b0e0b23b12af547a6dc7736fad9eaf76b14a56f310e531e6db098">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>assistants.test.ts</strong><dd><code>Added
`updatePageContext` to AI assistants tests</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-95533e004b514add57a2c87201a68cac11c20ffa458afd78e045ed89559e7546">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>auto-embeddings.test.ts</strong><dd><code>Added
`updatePageContext` to auto-embeddings tests</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-d3a5b860634fd36dd33ac9236210632eb5f8ad322aa15bedfc61a8e2c60dbd68">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>ban-user.test.ts</strong><dd><code>Refactored ban-user tests
with `gotoAuthURL`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-8d8d853b89f4a44454e4400182cbfe900f3c15eebe04d43a8d43f9c782b39f57">+16/-6</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>create-user.test.ts</strong><dd><code>Refactored create-user
tests with `gotoAuthURL`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-b5d83f9ceb9d621a5fe72789a6c961773548d7f459c72fad953b2a09694ff0a7">+2/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>delete-user.test.ts</strong><dd><code>Refactored delete-user
tests with `gotoAuthURL`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-a9d249f139e75a681888115b925e171c856c94f99c4077be6d954be4e58e0d74">+2/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>edit-user.test.ts</strong><dd><code>Refactored edit-user
tests with `gotoAuthURL`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-fc232b4d225c1367489733ede6bf5ebe88967b0353aa76c88c5e712c35b31be5">+2/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>verify-user.test.ts</strong><dd><code>Refactored verify-user
tests with `gotoAuthURL`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-a8425690a42ed772a97d3a17f062cb5713cc3180032c1d5eb1ef3f6d55cc110e">+2/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>create-table.test.ts</strong><dd><code>Added
`updatePageContext` to create-table tests</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-1e7aa9f3e379ca90a94b82c14be48e2c98a722d85ee1b0785a082b7076d8e58c">+6/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>delete-table.test.ts</strong><dd><code>Added
`updatePageContext` to delete-table tests</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-9e8c87f8e8f11bcfa2b7b2e5cf9dffe54a0fdeb3385ccb82b74e4e1c18fb9c43">+7/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>permissions-table.test.ts</strong><dd><code>Added
`updatePageContext` to permissions-table tests</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-f4b586f5b8f3bb97ddf64f8f38c461ac0424e101789f61e325d1b80bb8dc1047">+2/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>overview.test.ts</strong><dd><code>Integrated
`updatePageContext` in overview tests</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-4c6f1ff0b9d3b7fc7517aa50d9002bed56902f5b31557fa460f633f98da9cf01">+4/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>run.test.ts</strong><dd><code>Integrated `updatePageContext`
in run tests</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/3248/files#diff-3b81821630a8e66e8f580609a834499bdfec9ac228ff07b99f398ec07c329095">+2/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>1
files</summary><table>
<tr>
<td><strong>utils.ts</strong><dd><code>Added `updatePageContext` and
`gotoAuthURL` utility functions</code></dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3248/files#diff-490448aa83585151d8c61d698273c43486fdcac6a5d28a9b7e5be2729bbffd12">+13/-0</a>&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>
2025-03-20 14:46:42 +01:00
Alex
9b24807562 fix (dashboard) Update DatabasePiTRSettings.tsx to point to actual PiTR documentation (#3243)
Updated the docsLink of `DatabasePiTRSettings.tsx` to point to the
actual documentation of PiTR

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2025-03-18 12:31:53 +01:00
1569 changed files with 13817 additions and 97725 deletions

View File

@@ -1,5 +0,0 @@
---
'@nhost/dashboard': minor
---
feat: add empty string as default value for text in databases

View File

@@ -1,5 +0,0 @@
---
'@nhost/dashboard': minor
---
fix: update babel dependencies to address security audit vulnerabilities

View File

@@ -7,19 +7,20 @@ on:
- 'assets/**' - 'assets/**'
- '**.md' - '**.md'
- 'LICENSE' - 'LICENSE'
- 'docs/**'
pull_request: pull_request:
types: [opened, synchronize] types: [opened, synchronize]
paths-ignore: paths-ignore:
- 'assets/**' - 'assets/**'
- '**.md' - '**.md'
- 'LICENSE' - 'LICENSE'
- 'docs/**'
env: env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost TURBO_TEAM: nhost
NEXT_PUBLIC_ENV: dev NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1 NEXT_TELEMETRY_DISABLED: 1
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }} 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_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }} NHOST_TEST_ORGANIZATION_NAME: ${{ vars.NHOST_TEST_ORGANIZATION_NAME }}
NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }} NHOST_TEST_ORGANIZATION_SLUG: ${{ vars.NHOST_TEST_ORGANIZATION_SLUG }}
@@ -29,6 +30,7 @@ env:
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }} NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }} NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }} NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
jobs: jobs:
build: build:
@@ -170,6 +172,10 @@ jobs:
- name: Set Dashboard Preview URL - name: Set Dashboard Preview URL
if: steps.fetch-dashboard-preview-url.outputs.preview_url != '' if: steps.fetch-dashboard-preview-url.outputs.preview_url != ''
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
- name: Run Upgrade project Dashboard e2e tests
if: matrix.package.path == 'dashboard'
timeout-minutes: 10
run: pnpm --filter="${{ matrix.package.name }}" run e2e:upgrade-project
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo # * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
- name: Run e2e tests - name: Run e2e tests
timeout-minutes: 20 timeout-minutes: 20
@@ -178,13 +184,16 @@ jobs:
- name: Run Local Dashboard e2e tests - name: Run Local Dashboard e2e tests
if: matrix.package.path == 'dashboard' if: matrix.package.path == 'dashboard'
timeout-minutes: 5 timeout-minutes: 5
run: | run: pnpm --filter="${{ matrix.package.name }}" run e2e:local
pnpm --filter="${{ matrix.package.name }}" run e2e-local
- name: Stop Nhost CLI - name: Stop Nhost CLI
if: matrix.package.path == 'dashboard' if: matrix.package.path == 'dashboard'
working-directory: ./nhost-test-project working-directory: ./nhost-test-project
run: nhost down run: nhost down
- name: Stop Nhost CLI for packages
if: always() && (matrix.package.path == 'packages/hasura-auth-js' || matrix.package.path == 'packages/hasura-storage-js')
working-directory: ./${{ matrix.package.path }}
run: nhost down
- id: file-name - id: file-name
if: ${{ failure() }} if: ${{ failure() }}
name: Transform package name into a valid file name name: Transform package name into a valid file name

View File

@@ -56,3 +56,31 @@ jobs:
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }} vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }} vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }} vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
- name: Send Discord notification (success)
if: success()
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_PRODUCTION }}
embed-title: "Dashboard Deployment"
embed-description: |
**Status**: success
**Triggered by**: ${{ github.actor }}
**Inputs**:
- Git Ref: ${{ inputs.git_ref }}
embed-color: '5763719'
- name: Send Discord notification (failure)
if: failure()
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_PRODUCTION }}
embed-title: "Dashboard Deployment"
embed-description: |
**Status**: failure
**Triggered by**: ${{ github.actor }}
**Inputs**:
- Git Ref: ${{ inputs.git_ref }}
embed-color: '15548997'

View File

@@ -31,7 +31,7 @@ PRs to our libraries are always welcome and can be a quick way to get your fix o
- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. - Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both.
- Add unit or integration tests for fixed or changed functionality (if a test suite exists). - Add unit or integration tests for fixed or changed functionality (if a test suite exists).
- Address a single concern in the least number of changed lines as possible. - Address a single concern in the least number of changed lines as possible.
- Include documentation in the repo or on our [docs site](https://docs.nhost.io/get-started). - Include documentation in the repo or on our [docs site](https://docs.nhost.io).
- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created).
For changes that address core functionality or require breaking changes (e.g., a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes. For changes that address core functionality or require breaking changes (e.g., a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes.

View File

@@ -14,10 +14,10 @@ The easiest way to install `pnpm` if it's not installed on your machine yet is t
$ npm install -g pnpm $ npm install -g pnpm
``` ```
### [Nhost CLI](https://docs.nhost.io/cli) ### [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
- The CLI is primarily used for running the E2E tests - The CLI is primarily used for running the E2E tests
- Please refer to the [installation guide](https://docs.nhost.io/get-started/cli-workflow/install-cli) if you have not installed it yet - Please refer to the [installation guide](https://docs.nhost.io/platform/cli/local-development) if you have not installed it yet
## File Structure ## File Structure

View File

@@ -4,7 +4,7 @@
# Nhost # Nhost
<a href="https://docs.nhost.io/introduction#quick-start-guides">Quickstart</a> <a href="https://docs.nhost.io/getting-started/overview">Quickstart</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="http://nhost.io/">Website</a> <a href="http://nhost.io/">Website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
@@ -36,7 +36,7 @@ Nhost consists of open source software:
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/) - Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/)
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage) - Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
- Serverless Functions: Node.js (JavaScript and TypeScript) - Serverless Functions: Node.js (JavaScript and TypeScript)
- [Nhost CLI](https://docs.nhost.io/development/cli/overview) for local development - [Nhost CLI](https://docs.nhost.io/platform/cli/local-development) for local development
## Architecture of Nhost ## Architecture of Nhost
@@ -89,25 +89,25 @@ await nhost.graphql.request(`{
Nhost is frontend agnostic, which means Nhost works with all frontend frameworks. Nhost is frontend agnostic, which means Nhost works with all frontend frameworks.
<div align="center"> <div align="center">
<a href="https://docs.nhost.io/guides/quickstarts/nextjs"><img src="assets/nextjs.svg"/></a> <a href="https://docs.nhost.io/getting-started/quickstart/nextjs"><img src="assets/nextjs.svg"/></a>
<a href="https://docs.nhost.io/reference/javascript"><img src="assets/nuxtjs.svg"/></a> <a href="https://docs.nhost.io/reference/javascript/nhost-js/nhost-client"><img src="assets/nuxtjs.svg"/></a>
<a href="https://docs.nhost.io/guides/quickstarts/react"><img src="assets/react.svg"/></a> <a href="https://docs.nhost.io/getting-started/quickstart/react"><img src="assets/react.svg"/></a>
<a href="https://docs.nhost.io/reference/javascript"><img src="assets/react-native.svg"/></a> <a href="https://docs.nhost.io/getting-started/quickstart/reactnative"><img src="assets/react-native.svg"/></a>
<a href="https://docs.nhost.io/reference/javascript"><img src="assets/svelte.svg"/></a> <a href="https://docs.nhost.io/reference/javascript/nhost-js/nhost-client"><img src="assets/svelte.svg"/></a>
<a href="https://docs.nhost.io/guides/quickstarts/vue"><img src="assets/vuejs.svg"/></a> <a href="https://docs.nhost.io/getting-started/quickstart/vue"><img src="assets/vuejs.svg"/></a>
</div> </div>
# Resources # Resources
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/cli) - Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
## Nhost Clients ## Nhost Clients
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript) - [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/nhost-client)
- [Dart and Flutter](https://github.com/nhost/nhost-dart) - [Dart and Flutter](https://github.com/nhost/nhost-dart)
- [React](https://docs.nhost.io/reference/react) - [React](https://docs.nhost.io/reference/react/nhost-client)
- [Next.js](https://docs.nhost.io/reference/nextjs) - [Next.js](https://docs.nhost.io/reference/nextjs/nhost-client)
- [Vue](https://docs.nhost.io/reference/vue) - [Vue](https://docs.nhost.io/reference/vue/nhost-client)
## Integrations ## Integrations
@@ -140,7 +140,7 @@ This repository, and most of our other open source projects, are licensed under
Here are some ways of contributing to making Nhost better: Here are some ways of contributing to making Nhost better:
- **[Try out Nhost](https://docs.nhost.io/introduction)**, and think of ways to make the service better. Let us know here on GitHub. - **[Try out Nhost](https://docs.nhost.io)**, and think of ways to make the service better. Let us know here on GitHub.
- Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from. - Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from.
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) and our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) for more details about how to contribute. We're looking forward to your contribution! - Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) and our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) for more details about how to contribute. We're looking forward to your contribution!

View File

@@ -1,5 +1,58 @@
# @nhost/dashboard # @nhost/dashboard
## 2.28.0
### Minor Changes
- 8552678: feat: dashboard: add additional events to segment
- 0bf2808: chore: refresh metadata before end-to-end tests
- 72a365c: fix: correct graphql page roles dropdown's source
- cef6471: fix: dashboard: add anonid to user's metadata
### Patch Changes
- d9eb906: fix: update vite and nextjs because of vulnerability
- 233232b: feat (dashboard): improve Upgrade project dialog
- Updated dependencies [d9eb906]
- @nhost/nextjs@2.2.7
- @nhost/react-apollo@17.0.4
## 2.27.0
### Minor Changes
- 013e1c1: fix: update vite and image-size dependencies to address security audit vulnerabilities
- 4fd176b: chore: re-add user event ci tests, updated sveltekit example tests to e2e suite
### Patch Changes
- a1333df: fix: update vite because of vulnerability
- 0420e4f: fix (dashboard): Display the selected date's month in the datetime picker component
- @nhost/react-apollo@17.0.3
- @nhost/nextjs@2.2.6
## 2.26.0
### Minor Changes
- 7b9cdf1: chore: remove legacy workspaces
- 1c4f321: fix: update vite to fix audit vulnerabilities
## 2.25.0
### Minor Changes
- 34fdcb8: chore: add prettier plugins as devDependencies to root of monorepo
- 4937c5e: fix: stop content overflowing in projects and database permissions page
- 1542132: fix: update babel dependencies to address security audit vulnerabilities
### Patch Changes
- 78436ca: chore (dashboard): add tests and small updates to PiTR settings and restore page
- b5a3895: chore (dashboard): update page context after each navigation
- 9b24807: chore: fix link to PiTR documentation
- ea65846: chore (dashboard): update nextjs to fix middleware exploit
## 2.17.0 ## 2.17.0
### Minor Changes ### Minor Changes

View File

@@ -38,7 +38,7 @@ These files are added to `.gitignore`, so you don't need to worry about committi
### Enable Local Development ### Enable Local Development
You can connect the Nhost Dashboard to your **locally running** Nhost backend in a few steps. Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli#installation). You can connect the Nhost Dashboard to your **locally running** Nhost backend in a few steps. Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli/local-development).
First, you need to run the following command to start your backend locally: First, you need to run the following command to start your backend locally:
@@ -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_DASHBOARD_URL=<test_dashboard_url>
NHOST_TEST_USER_EMAIL=<test_user_email> NHOST_TEST_USER_EMAIL=<test_user_email>
NHOST_TEST_USER_PASSWORD=<test_user_password> 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_NAME=<test_project_name>
NHOST_TEST_PROJECT_SUBDOMAIN=<test_project_subdomain>
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret> 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_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_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_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_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 - `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: End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:

View File

@@ -1,22 +1,10 @@
import { expect, test } from '@/e2e/fixtures/auth-hook';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page; test('should be able to create then delete a personal access token', async ({
authenticatedNhostPage: page,
test.beforeAll(async ({ browser }) => { }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
});
test.afterAll(async () => {
await page.close();
});
test('should be able to create then delete a personal access token', async () => {
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
await page.getByRole('banner').getByRole('button').last().click(); await page.getByRole('banner').getByRole('button').last().click();
await page.getByRole('link', { name: /account settings/i }).click(); await page.getByRole('link', { name: /account settings/i }).click();

View File

@@ -1,17 +1,8 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { expect, test } from '@/e2e/fixtures/auth-hook';
import { navigateToProject } from '@/e2e/utils'; import { navigateToProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await navigateToProject({ await navigateToProject({
page, page,
orgSlug: TEST_ORGANIZATION_SLUG, orgSlug: TEST_ORGANIZATION_SLUG,
@@ -23,11 +14,9 @@ test.beforeEach(async () => {
await page.waitForURL(AIRoute); await page.waitForURL(AIRoute);
}); });
test.afterAll(async () => { test('should create and delete an Assistant', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should create and delete an Assistant', async () => {
await page.getByRole('link', { name: 'Assistants' }).click(); await page.getByRole('link', { name: 'Assistants' }).click();
await expect(page.getByText(/no assistants are configured/i)).toBeVisible(); await expect(page.getByText(/no assistants are configured/i)).toBeVisible();

View File

@@ -1,17 +1,9 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { navigateToProject } from '@/e2e/utils'; import { navigateToProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page; import { expect, test } from '@/e2e/fixtures/auth-hook';
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await navigateToProject({ await navigateToProject({
page, page,
orgSlug: TEST_ORGANIZATION_SLUG, orgSlug: TEST_ORGANIZATION_SLUG,
@@ -23,11 +15,9 @@ test.beforeEach(async () => {
await page.waitForURL(AIRoute); await page.waitForURL(AIRoute);
}); });
test.afterAll(async () => { test('should create and delete an Auto-Embeddings', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should create and delete an Auto-Embeddings', async () => {
await page.getByRole('button', { name: 'Add a new Auto-Embeddings' }).click(); await page.getByRole('button', { name: 'Add a new Auto-Embeddings' }).click();
await page.getByLabel('Name').fill('test'); await page.getByLabel('Name').fill('test');

View File

@@ -1,13 +1,14 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { createUser, generateTestEmail } from '@/e2e/utils'; import { createUser, generateTestEmail, gotoAuthURL } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import test, { expect } from '@playwright/test';
test('should be able to ban and unban a user', async ({ page }) => { test.beforeEach(async ({ authenticatedNhostPage: page }) => {
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`; await gotoAuthURL(page);
await page.goto(authUrl); });
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
test('should be able to ban and unban a user', async ({
authenticatedNhostPage: page,
}) => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();

View File

@@ -1,26 +1,12 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { createUser, generateTestEmail } from '@/e2e/utils'; import { createUser, generateTestEmail, gotoAuthURL } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import test, { expect } from '@playwright/test';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await gotoAuthURL(page);
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
}); });
test.beforeEach(async () => { test('should create a user', async ({ authenticatedNhostPage: page }) => {
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
await page.goto(authUrl);
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
});
test.afterAll(async () => {
await page.close();
});
test('should create a user', async () => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();
@@ -31,7 +17,9 @@ test('should create a user', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should not be able to create a user with an existing email', async () => { test('should not be able to create a user with an existing email', async ({
authenticatedNhostPage: page,
}) => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();

View File

@@ -1,26 +1,15 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { createUser, generateTestEmail, gotoAuthURL } from '@/e2e/utils';
import { createUser, generateTestEmail } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import test, { expect } from '@playwright/test';
let page: Page; import { expect, test } from '@/e2e/fixtures/auth-hook';
test.beforeAll(async ({ browser }) => { test.beforeEach(async ({ authenticatedNhostPage: page }) => {
page = await browser.newPage(); await gotoAuthURL(page);
}); });
test.beforeEach(async () => { test('should be able to delete a user', async ({
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`; authenticatedNhostPage: page,
await page.goto(authUrl); }) => {
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
});
test.afterAll(async () => {
await page.close();
});
test('should be able to delete a user', async () => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();
@@ -52,7 +41,9 @@ test('should be able to delete a user', async () => {
).not.toBeVisible(); ).not.toBeVisible();
}); });
test('should be able to delete a user from the details page', async () => { test('should be able to delete a user from the details page', async ({
authenticatedNhostPage: page,
}) => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();

View File

@@ -1,26 +1,14 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { createUser, generateTestEmail } from '@/e2e/utils'; import { createUser, generateTestEmail, gotoAuthURL } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import test, { expect } from '@playwright/test';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await gotoAuthURL(page);
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
}); });
test.beforeEach(async () => { test('should be able to edit user roles from the details page', async ({
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`; authenticatedNhostPage: page,
await page.goto(authUrl); }) => {
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
});
test.afterAll(async () => {
await page.close();
});
test('should be able to edit user roles from the details page', async () => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();

View File

@@ -1,26 +1,14 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { createUser, generateTestEmail } from '@/e2e/utils'; import { createUser, generateTestEmail, gotoAuthURL } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await gotoAuthURL(page);
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
}); });
test.beforeEach(async () => { test('should be able to verify the email of a user', async ({
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`; authenticatedNhostPage: page,
await page.goto(authUrl); }) => {
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
});
test.afterAll(async () => {
await page.close();
});
test('should be able to verify the email of a user', async () => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();
@@ -50,7 +38,9 @@ test('should be able to verify the email of a user', async () => {
).toBeChecked(); ).toBeChecked();
}); });
test('should be able to verify the phone number of a user', async () => { test('should be able to verify the phone number of a user', async ({
authenticatedNhostPage: page,
}) => {
const email = generateTestEmail(); const email = generateTestEmail();
const password = faker.internet.password(); const password = faker.internet.password();
const phoneNumber = faker.phone.number(); const phoneNumber = faker.phone.number();

View File

@@ -1,35 +1,18 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { navigateToProject, prepareTable } from '@/e2e/utils'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { prepareTable } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { snakeCase } from 'snake-case'; import { snakeCase } from 'snake-case';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await navigateToProject({
page,
orgSlug: TEST_ORGANIZATION_SLUG,
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
});
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`; const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
await page.goto(databaseRoute); await page.goto(databaseRoute);
await page.waitForURL(databaseRoute); await page.waitForURL(databaseRoute);
}); });
test.afterAll(async () => { test('should create a simple table', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should create a simple table', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -57,7 +40,9 @@ test('should create a simple table', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should create a table with unique constraints', async () => { test('should create a table with unique constraints', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -86,7 +71,9 @@ test('should create a table with unique constraints', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should create a table with nullable columns', async () => { test('should create a table with nullable columns', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -115,7 +102,9 @@ test('should create a table with nullable columns', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should create a table with an identity column', async () => { test('should create a table with an identity column', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -148,7 +137,9 @@ test('should create a table with an identity column', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should create table with foreign key constraint', async () => { test('should create table with foreign key constraint', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -221,7 +212,9 @@ test('should create table with foreign key constraint', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should not be able to create a table with a name that already exists', async () => { test('should not be able to create a table with a name that already exists', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();

View File

@@ -1,35 +1,17 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { deleteTable, navigateToProject, prepareTable } from '@/e2e/utils'; import { deleteTable, prepareTable } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { snakeCase } from 'snake-case'; import { snakeCase } from 'snake-case';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await navigateToProject({
page,
orgSlug: TEST_ORGANIZATION_SLUG,
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
});
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`; const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
await page.goto(databaseRoute); await page.goto(databaseRoute);
await page.waitForURL(databaseRoute); await page.waitForURL(databaseRoute);
}); });
test.afterAll(async () => { test('should delete a table', async ({ authenticatedNhostPage: page }) => {
await page.close();
});
test('should delete a table', async () => {
const tableName = snakeCase(faker.lorem.words(3)); const tableName = snakeCase(faker.lorem.words(3));
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
@@ -65,7 +47,9 @@ test('should delete a table', async () => {
).not.toBeVisible(); ).not.toBeVisible();
}); });
test('should not be able to delete a table if other tables have foreign keys referencing it', async () => { test('should not be able to delete a table if other tables have foreign keys referencing it', async ({
authenticatedNhostPage: page,
}) => {
test.setTimeout(60000); test.setTimeout(60000);
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();

View File

@@ -1,39 +1,18 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { import { expect, test } from '@/e2e/fixtures/auth-hook';
clickPermissionButton, import { clickPermissionButton, prepareTable } from '@/e2e/utils';
navigateToProject,
prepareTable,
} from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { snakeCase } from 'snake-case'; import { snakeCase } from 'snake-case';
let page: Page; test.beforeEach(async ({ authenticatedNhostPage: page }) => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await navigateToProject({
page,
orgSlug: TEST_ORGANIZATION_SLUG,
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
});
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`; const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
await page.goto(databaseRoute); await page.goto(databaseRoute);
await page.waitForURL(databaseRoute); await page.waitForURL(databaseRoute);
}); });
test.afterAll(async () => { test('should create a table with role permissions to select row', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should create a table with role permissions to select row', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
@@ -79,7 +58,9 @@ test('should create a table with role permissions to select row', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should create a table with role permissions and a custom check to select rows', async () => { test('should create a table with role permissions and a custom check to select rows', async ({
authenticatedNhostPage: page,
}) => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();

View File

@@ -4,7 +4,7 @@
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL; 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; export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME;
@@ -40,3 +40,7 @@ export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL;
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD; export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG; export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG;
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS;
export const TEST_FREE_USER_EMAILS = JSON.parse(freeUserEmails);

View File

@@ -0,0 +1,22 @@
import { TEST_DASHBOARD_URL, TEST_PERSONAL_ORG_SLUG } from '@/e2e/env';
import { type Page, test as base } from '@playwright/test';
export const AUTH_CONTEXT = 'e2e/.auth/user.json';
export const test = base.extend<{ authenticatedNhostPage: Page }>({
authenticatedNhostPage: async ({ browser }, use) => {
const context = await browser.newContext({ storageState: AUTH_CONTEXT });
const page = await context.newPage();
await page.goto('/');
await page.waitForURL(
`${TEST_DASHBOARD_URL}/orgs/${TEST_PERSONAL_ORG_SLUG}/projects`,
{ waitUntil: 'networkidle' },
);
await use(page);
// update the context to get the new refresh token
await page.context().storageState({ path: AUTH_CONTEXT });
await page.close();
},
});
export { expect } from '@playwright/test';

View File

@@ -1,15 +1,8 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import type { Page } from '@playwright/test'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { expect, test } from '@playwright/test'; import { navigateToProject } from '@/e2e/utils';
import { navigateToProject } from '../utils';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
await page.goto('/');
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
await navigateToProject({ await navigateToProject({
page, page,
orgSlug: TEST_ORGANIZATION_SLUG, orgSlug: TEST_ORGANIZATION_SLUG,
@@ -17,11 +10,9 @@ test.beforeAll(async ({ browser }) => {
}); });
}); });
test.afterAll(async () => { test('should show the navtree with all links visible', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should show the navtree with all links visible', async () => {
const navLocator = page.getByLabel('Navigation Tree'); const navLocator = page.getByLabel('Navigation Tree');
await expect(navLocator).toBeVisible(); await expect(navLocator).toBeVisible();
@@ -42,16 +33,20 @@ test('should show the navtree with all links visible', async () => {
'Settings', 'Settings',
]; ];
// eslint-disable-next-line no-restricted-syntax
for (const linkName of links) { for (const linkName of links) {
const link = const link =
linkName === 'Settings' linkName === 'Settings'
? page.getByRole('link', { name: linkName }).first() ? page.getByRole('link', { name: linkName }).first()
: page.getByRole('link', { name: linkName }); : page.getByRole('link', { name: linkName });
// eslint-disable-next-line no-await-in-loop
await expect(link).toBeVisible(); await expect(link).toBeVisible();
} }
}); });
test("should show the project's region and subdomain", async () => { test("should show the project's region and subdomain", async ({
authenticatedNhostPage: page,
}) => {
await expect(page.locator('p:has-text("Region") + div p').nth(0)).toHaveText( await expect(page.locator('p:has-text("Region") + div p').nth(0)).toHaveText(
/frankfurt \(eu-central-1\)/i, /frankfurt \(eu-central-1\)/i,
); );
@@ -60,7 +55,9 @@ test("should show the project's region and subdomain", async () => {
).toHaveText(/[a-z]{20}/i); ).toHaveText(/[a-z]{20}/i);
}); });
test('should not have a GitHub repository connected', async () => { test('should not have a GitHub repository connected', async ({
authenticatedNhostPage: page,
}) => {
await expect( await expect(
page.getByRole('button', { name: /connect to github/i }).first(), page.getByRole('button', { name: /connect to github/i }).first(),
).toBeVisible(); ).toBeVisible();

View File

@@ -1,33 +1,15 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env'; import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import type { Page } from '@playwright/test'; import { expect, test } from '@/e2e/fixtures/auth-hook';
import { expect, test } from '@playwright/test';
import { navigateToProject } from '../utils';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await navigateToProject({
page,
orgSlug: TEST_ORGANIZATION_SLUG,
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
});
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
const runRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/run`; const runRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/run`;
await page.goto(runRoute); await page.goto(runRoute);
await page.waitForURL(runRoute); await page.waitForURL(runRoute);
}); });
test.afterAll(async () => { test('should create and delete a run service', async ({
await page.close(); authenticatedNhostPage: page,
}); }) => {
test('should create and delete a run service', async () => {
await page.getByRole('button', { name: 'Add service' }).first().click(); await page.getByRole('button', { name: 'Add service' }).first().click();
await expect(page.getByText(/create a new service/i)).toBeVisible(); await expect(page.getByText(/create a new service/i)).toBeVisible();
await page.getByPlaceholder(/service name/i).click(); await page.getByPlaceholder(/service name/i).click();

View File

@@ -0,0 +1,40 @@
import { TEST_PROJECT_ADMIN_SECRET, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { test as setup } from '@playwright/test';
setup('refresh metadata', async () => {
try {
await fetch(
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
{
method: 'POST',
headers: {
'x-hasura-admin-secret': TEST_PROJECT_ADMIN_SECRET,
},
body: JSON.stringify({
args: [
{
type: 'reload_metadata',
args: {
reload_remote_schemas: [],
reload_sources: [],
},
},
{
args: {},
type: 'get_inconsistent_metadata',
},
],
source: 'default',
type: 'bulk',
}),
},
);
} catch (error) {
// Log safe error information
console.error(
'Failed to refresh metadata:',
error instanceof Error ? error.message : 'Unknown error',
);
throw new Error('Failed to refresh metadata');
}
});

View File

@@ -1,49 +1,23 @@
import { import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
TEST_DASHBOARD_URL, import { expect, test as teardown } from '@/e2e/fixtures/auth-hook';
TEST_ORGANIZATION_SLUG,
TEST_PROJECT_SUBDOMAIN,
} from '@/e2e/env';
import { navigateToProject } from '@/e2e/utils';
import { type Page, expect, test as teardown } from '@playwright/test';
let page: Page;
teardown.beforeAll(async ({ browser }) => {
const context = await browser.newContext({
baseURL: TEST_DASHBOARD_URL,
storageState: 'e2e/.auth/user.json',
});
page = await context.newPage();
});
teardown.beforeEach(async () => {
await page.goto('/');
await navigateToProject({
page,
orgSlug: TEST_ORGANIZATION_SLUG,
projectSubdomain: TEST_PROJECT_SUBDOMAIN,
});
teardown.beforeEach(async ({ authenticatedNhostPage: page }) => {
const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`; const databaseRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default`;
await page.goto(databaseRoute); await page.goto(databaseRoute);
await page.waitForURL(databaseRoute); await page.waitForURL(databaseRoute);
}); });
teardown.afterAll(async () => { teardown(
await page.close(); 'clean up database tables',
}); async ({ authenticatedNhostPage: page }) => {
await page.getByRole('link', { name: /sql editor/i }).click();
teardown('clean up database tables', async () => { await page.waitForURL(
await page.getByRole('link', { name: /sql editor/i }).click(); `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/editor`,
);
await page.waitForURL( const inputField = page.locator('[contenteditable]');
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/editor`, await inputField.fill(`
);
const inputField = page.locator('[contenteditable]');
await inputField.fill(`
DO $$ DECLARE DO $$ DECLARE
tablename text; tablename text;
BEGIN BEGIN
@@ -56,6 +30,7 @@ teardown('clean up database tables', async () => {
END $$; END $$;
`); `);
await page.locator('button[type="button"]', { hasText: /run/i }).click(); await page.locator('button[type="button"]', { hasText: /run/i }).click();
await expect(page.getByText(/success/i)).toBeVisible(); await expect(page.getByText(/success/i)).toBeVisible();
}); },
);

View File

@@ -0,0 +1,144 @@
import { expect, test } from '@/e2e/fixtures/auth-hook';
import {
getCardExpiration,
getFreeUserStarterOrgSlug,
getNewOrgSlug,
getNewProjectName,
getNewProjectSlug,
getOrgSlugFromUrl,
getProjectSlugFromUrl,
gotoUrl,
loginWithFreeUser,
setNewOrgSlug,
setNewProjectName,
setNewProjectSlug,
} from '@/e2e/utils';
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
await loginWithFreeUser(page);
});
test('should create a new project', async () => {
await await gotoUrl(
page,
`/orgs/${getFreeUserStarterOrgSlug()}/projects/new`,
);
const projectName = faker.lorem.words(3);
await page.getByLabel('Project Name').fill(projectName);
await page.getByText('Create Project').click();
expect(await page.getByText('Creating the project...')).toBeVisible();
expect(await page.getByText('Internal info')).toBeVisible();
await page.waitForSelector('button:has-text("Upgrade project")', {
timeout: 120000,
});
const newProjectSlug = getProjectSlugFromUrl(await page.url());
setNewProjectSlug(newProjectSlug);
setNewProjectName(projectName);
});
test('should upgrade the project', async () => {
await gotoUrl(
page,
`/orgs/${getFreeUserStarterOrgSlug()}/projects/${getNewProjectSlug()}`,
);
const upgradeProject = await page.getByText('Upgrade project');
expect(upgradeProject).toBeVisible();
await upgradeProject.click();
await page.waitForSelector('h2:has-text("Upgrade project")');
await page.getByRole('button', { name: 'Continue' }).click();
await page.waitForSelector('h2:has-text("New Organization")');
const newOrgName = faker.lorem.words(3);
await page.getByLabel('Organization Name').fill(newOrgName);
await page.getByText('Create organization').click();
await page.waitForSelector('button:has-text("Create organization")', {
state: 'hidden',
});
const stripeFrame = await page
.frameLocator('iframe[name="embedded-checkout"]')
.first();
await stripeFrame.getByText('Subscribe to Nhost');
await stripeFrame.getByLabel('Email').fill(faker.internet.email());
await stripeFrame
.getByPlaceholder('1234 1234 1234 1234')
.fill('4242424242424242');
await stripeFrame.getByPlaceholder('MM / YY').fill(getCardExpiration());
await stripeFrame.getByPlaceholder('CVC').fill('123');
await stripeFrame
.getByPlaceholder('Full name on card')
.fill('EndyTo EndyTest');
await stripeFrame.locator('#billingCountry').scrollIntoViewIfNeeded();
// Need to comment out for local testing START
await stripeFrame.getByPlaceholder('Address', { exact: true }).click();
await stripeFrame.locator('span:has-text("Enter address manually")');
await stripeFrame.getByText('Enter address manually').click();
await stripeFrame
.getByPlaceholder('Address line 1', { exact: true })
.fill('123 Main Street');
await stripeFrame
.getByPlaceholder('City', { exact: true })
.fill('Springfield');
await stripeFrame.getByPlaceholder('ZIP', { exact: true }).fill('62701');
// local Comment end
await stripeFrame
.getByTestId('hosted-payment-submit-button')
.scrollIntoViewIfNeeded();
await stripeFrame
.getByTestId('hosted-payment-submit-button')
.click({ force: true });
await page.waitForSelector('h2:has-text("Upgrade project")');
await page.waitForSelector(
'div:has-text("Organization created successfully.")',
);
await page.waitForSelector(
'div:has-text("Project has been upgraded successfully!")',
);
await page.getByRole('button', { name: 'Create project' });
await page.waitForSelector(`div:has-text("${newOrgName}")`);
await page.waitForSelector(`p:has-text("${getNewProjectName()}")`);
setNewOrgSlug(getOrgSlugFromUrl(await page.url()));
});
test('should delete the new organization', async () => {
await gotoUrl(page, `/orgs/${getNewOrgSlug()}/projects`);
await page.getByRole('link', { name: 'Settings' }).click();
await page.waitForSelector('h3:has-text("Delete Organization")');
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForSelector('h2:has-text("Delete Organization")');
expect(await page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel("I'm sure I want to delete this Organization").click();
expect(await page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel('I understand this action cannot be undone').click();
expect(await page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await page.getByTestId('deleteOrgButton').click();
await page.waitForSelector('div:has-text("Deleting the organization")');
await page.waitForSelector(
'div:has-text("Successfully deleted the organization")',
);
await page.waitForSelector(`div:has-text("Personal Organization")`);
});

View File

@@ -1,5 +1,12 @@
import {
TEST_FREE_USER_EMAILS,
TEST_ORGANIZATION_SLUG,
TEST_PROJECT_SUBDOMAIN,
TEST_USER_PASSWORD,
} from '@/e2e/env';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test'; import { type Page, expect } from '@playwright/test';
import { add, format } from 'date-fns-v4';
/** /**
* Open a project by navigating to the project's overview page. * Open a project by navigating to the project's overview page.
@@ -211,3 +218,97 @@ export async function clickPermissionButton({
.locator('button') .locator('button')
.click(); .click();
} }
export async function gotoAuthURL(page: Page) {
const authUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/users`;
await page.goto(authUrl);
await page.waitForURL(authUrl, { waitUntil: 'networkidle' });
}
export async function gotoUrl(page: Page, url: string) {
await page.url;
await page.goto(url);
await page.waitForURL(url, { waitUntil: 'networkidle' });
}
let newOrgSlug: string;
export function getNewOrgSlug() {
return newOrgSlug;
}
export function setNewOrgSlug(slug: string) {
newOrgSlug = slug;
}
let freeUserStarterOrgSlug: string;
export function getFreeUserStarterOrgSlug() {
return freeUserStarterOrgSlug;
}
export function setFreeUserStarterOrgSlug(slug: string) {
freeUserStarterOrgSlug = slug;
}
let newProjectSlug: string;
export function getNewProjectSlug() {
return newProjectSlug;
}
export function setNewProjectSlug(slug: string) {
newProjectSlug = slug;
}
export function getProjectSlugFromUrl(url: string) {
const [, projectSlug] = url.split('/projects/');
return projectSlug;
}
export function getOrgSlugFromUrl(url: string) {
const orgSlug = url.split('/orgs/')[1].replace('/projects', '');
return orgSlug;
}
export function getCardExpiration() {
const now = add(new Date(), { years: 3 });
return format(now, 'MMyy');
}
let newProjectName: string;
export function getNewProjectName() {
return newProjectName;
}
export function setNewProjectName(name: string) {
newProjectName = name;
}
function getRandomUserIndex(): number {
return Math.floor(Math.random() * 5);
}
export async function loginWithFreeUser(page: Page) {
const userIndex = getRandomUserIndex();
const freeUserEmail = TEST_FREE_USER_EMAILS[userIndex];
// eslint-disable-next-line no-console
console.log(`Selected userIndex: ${userIndex}`);
await page.goto('/');
await page.waitForURL('/signin');
await page.getByRole('link', { name: /continue with email/i }).click();
await page.waitForURL('/signin/email');
await page.getByLabel('Email').fill(freeUserEmail);
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
expect(
await page.getByRole('button', { name: 'Create project' }),
).not.toBeVisible();
await page.waitForSelector('h2:has-text("Welcome to")', { timeout: 20000 });
setFreeUserStarterOrgSlug(getOrgSlugFromUrl(await page.url()));
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/dashboard", "name": "@nhost/dashboard",
"version": "2.24.0", "version": "2.28.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -16,8 +16,10 @@
"storybook": "start-storybook -p 6006 -s public", "storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook", "build-storybook": "build-storybook",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps", "install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts", "e2e:tests": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts",
"e2e-local": "pnpm install-browsers && pnpm playwright test --config=playwright.local.config.ts" "e2e": "pnpm e2e:tests --project=main",
"e2e:local": "pnpm e2e:tests --project=local",
"e2e:upgrade-project": "pnpm e2e:tests --project=upgrade-project"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.9.9", "@apollo/client": "^3.9.9",
@@ -87,7 +89,7 @@
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lucide-react": "^0.416.0", "lucide-react": "^0.416.0",
"next": "^14.2.22", "next": "^14.2.26",
"next-nprogress-bar": "^2.3.13", "next-nprogress-bar": "^2.3.13",
"next-seo": "^6.5.0", "next-seo": "^6.5.0",
"next-themes": "^0.3.0", "next-themes": "^0.3.0",
@@ -96,7 +98,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-children-utilities": "^2.10.0", "react-children-utilities": "^2.10.0",
"react-complex-tree": "^2.4.5", "react-complex-tree": "^2.4.5",
"react-day-picker": "8.10.1", "react-day-picker": "9.6.3",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-hook-form": "^7.53.0", "react-hook-form": "^7.53.0",
@@ -112,7 +114,6 @@
"recoil-persist": "^5.1.0", "recoil-persist": "^5.1.0",
"rehype-highlight": "^7.0.0", "rehype-highlight": "^7.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"stripe": "^10.17.0", "stripe": "^10.17.0",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
@@ -157,7 +158,6 @@
"@types/react": "^18.2.73", "@types/react": "^18.2.73",
"@types/react-dom": "^18.2.23", "@types/react-dom": "^18.2.23",
"@types/react-table": "^7.7.20", "@types/react-table": "^7.7.20",
"@types/shell-quote": "^1.7.5",
"@types/testing-library__jest-dom": "^5.14.9", "@types/testing-library__jest-dom": "^5.14.9",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/validator": "^13.11.9", "@types/validator": "^13.11.9",
@@ -196,7 +196,7 @@
"tailwindcss": "^3.4.12", "tailwindcss": "^3.4.12",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0", "tsconfig-paths-webpack-plugin": "^4.1.0",
"vite": "^5.4.12", "vite": "^5.4.18",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^0.32.4" "vitest": "^0.32.4"
}, },

View File

@@ -17,7 +17,7 @@ export default defineConfig({
reporter: 'html', reporter: 'html',
use: { use: {
actionTimeout: 0, actionTimeout: 0,
trace: 'on-first-retry', trace: 'retain-on-failure',
baseURL: process.env.NHOST_TEST_DASHBOARD_URL, baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
launchOptions: { launchOptions: {
slowMo: 500, slowMo: 500,
@@ -34,13 +34,28 @@ export default defineConfig({
testMatch: ['**/teardown/*.teardown.ts'], testMatch: ['**/teardown/*.teardown.ts'],
}, },
{ {
name: 'chromium', name: 'main',
use: { use: {
...devices['Desktop Chrome'], ...devices['Desktop Chrome'],
storageState: 'e2e/.auth/user.json', storageState: 'e2e/.auth/user.json',
}, },
dependencies: ['setup'], dependencies: ['setup'],
grepInvert: [/Local Dashboard CLI e2e tests/], testIgnore: ['upgrade-project.test.ts', 'cli-local-dashboard.test.ts'],
},
{
name: 'local',
use: {
...devices['Desktop Chrome'],
baseURL: '', // Local dashboard URL
},
testMatch: 'cli-local-dashboard.test.ts',
},
{
name: 'upgrade-project',
testMatch: 'upgrade-project.test.ts',
use: {
...devices['Desktop Chrome'],
},
}, },
], ],
}); });

View File

@@ -1,31 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30 * 1000,
expect: {
timeout: 5000,
},
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'html',
use: {
actionTimeout: 0,
trace: 'on-first-retry',
baseURL: '', // Local dashboard URL
launchOptions: {
slowMo: 500,
},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
testMatch: ['**/e2e/cli-local-dashboard/**'],
},
],
});

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

@@ -0,0 +1,177 @@
import { isTZDate } from '@/components/common/TimePicker/time-picker-utils';
import { render, screen, TestUserEvent, waitFor } from '@/tests/testUtils';
import { isBefore, startOfDay } from 'date-fns-v4';
import { useState } from 'react';
import { TZDate } from 'react-day-picker';
import { vi } from 'vitest';
import DateTimePicker, { type DateTimePickerProps } from './DateTimePicker';
vi.mock('@/utils/timezoneUtils', async () => {
const actualTimezoneUtils = await vi.importActual<any>(
'@/utils/timezoneUtils',
);
return {
...actualTimezoneUtils,
guessTimezone: () => 'Europe/Helsinki',
};
});
const earliestBackupDate = '2025-03-13T02:00:05.000Z';
function TestComponent(
props: Omit<DateTimePickerProps, 'dateTime' | 'onDateTimeChange'>,
) {
const [dateTime, setDateTime] = useState(earliestBackupDate);
function isCalendarDayDisabled(date: Date | TZDate) {
if (isTZDate(date)) {
const utcDay = new Date(date.getTime()).toISOString();
const tzDate = new TZDate(utcDay, date.timeZone);
const earliestBackupDateInTz = new TZDate(
earliestBackupDate,
date.timeZone,
);
return isBefore(startOfDay(tzDate), startOfDay(earliestBackupDateInTz));
}
return isBefore(
startOfDay(new Date(date.getTime()).toISOString()),
startOfDay(earliestBackupDate),
);
}
return (
<>
<h1 data-testid="utcDate">{dateTime}</h1>
<DateTimePicker
{...props}
isCalendarDayDisabled={isCalendarDayDisabled}
dateTime={dateTime}
onDateTimeChange={setDateTime}
/>
</>
);
}
describe('DateTimePicker', () => {
test('when the date changes datetime is emitted in utc string format', async () => {
render(<TestComponent />);
const user = new TestUserEvent();
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
expect(
await screen.findByRole('button', { name: 'Select' }),
).toBeInTheDocument();
expect(await screen.getByText('March 2025')).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: 'Go to the Next Month' }),
);
expect(screen.getByText('April 2025')).toBeInTheDocument();
await user.click(await screen.getByText('13'));
const hoursInput = await screen.getByLabelText('Hours');
await user.type(hoursInput, '11');
const minutesInput = await screen.getByLabelText('Minutes');
await user.type(minutesInput, '12');
const secondsInput = await screen.getByLabelText('Seconds');
await user.type(secondsInput, '13');
await user.click(await screen.getByRole('button', { name: 'Select' }));
await waitFor(async () =>
expect(
await screen.queryByRole('button', { name: 'Select' }),
).not.toBeInTheDocument(),
);
expect(screen.getByTestId('utcDate')).toHaveTextContent(
'2025-04-13T08:12:13.000Z',
);
});
test('timezone can be changed and the calendar is updated', async () => {
await waitFor(() => render(<TestComponent withTimezone />));
const user = new TestUserEvent();
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
expect(await screen.findByText(/Timezone:/)).toBeInTheDocument();
expect(
await screen.findByTestId('timezoneSettingsButton'),
).toBeInTheDocument();
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
'Timezone: UTC+02:00',
);
expect(await screen.getByText('12')).toBeDisabled();
await user.click(await screen.findByTestId('timezoneSettingsButton'));
const tzInput = await screen.findByPlaceholderText('Search timezones...');
expect(tzInput).toBeInTheDocument();
await user.type(tzInput, 'America/Chicago{ArrowDown}{Enter}');
expect(
await screen.queryByPlaceholderText('Search timezones...'),
).not.toBeInTheDocument();
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
'Timezone: UTC-05:00',
);
const selectedDay = screen.getByText('12');
expect(selectedDay).not.toBeDisabled();
expect(await screen.getByText('11')).toBeDisabled();
const gridCell = selectedDay.closest('[role="gridcell"]');
expect(gridCell).toHaveClass('[&>button]:bg-primary');
});
test('Displays the correct time zone offset when changing the selected date from standard time (ST) to daylight saving time (DST)', async () => {
await waitFor(() => render(<TestComponent withTimezone />));
const user = new TestUserEvent();
await user.click(await screen.findByTestId('dateTimePickerTrigger'));
expect(await screen.findByText(/Timezone:/)).toBeInTheDocument();
expect(
await screen.findByTestId('timezoneSettingsButton'),
).toBeInTheDocument();
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
'Timezone: UTC+02:00',
);
expect(await screen.getByText('March 2025')).toBeInTheDocument();
await user.click(
screen.getByRole('button', { name: 'Go to the Next Month' }),
);
expect(screen.getByText('April 2025')).toBeInTheDocument();
await user.click(await screen.getByText('18'));
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
'Timezone: UTC+03:00',
);
await user.click(
screen.getByRole('button', { name: 'Go to the Previous Month' }),
);
expect(await screen.getByText('March 2025')).toBeInTheDocument();
await user.click(await screen.getByText('21'));
expect(await screen.findByText(/Timezone: /i)).toHaveTextContent(
'Timezone: UTC+02:00',
);
});
});

View File

@@ -27,8 +27,6 @@ export interface DateTimePickerProps {
align?: 'start' | 'center' | 'end'; align?: 'start' | 'center' | 'end';
validateDateFn?: (date: Date) => string; validateDateFn?: (date: Date) => string;
} }
// in: UTC datetime
// out: UTC dateTime
function DateTimePicker({ function DateTimePicker({
dateTime, dateTime,
@@ -49,6 +47,10 @@ function DateTimePicker({
}); });
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [timezone, setTimezone] = useState(
() => defaultTimezone || guessTimezone(),
);
function emitNewDateTime() { function emitNewDateTime() {
onDateTimeChange(new Date(date.getTime()).toISOString()); onDateTimeChange(new Date(date.getTime()).toISOString());
} }
@@ -73,6 +75,7 @@ function DateTimePicker({
function handleTimezoneChange(newTimezone: string) { function handleTimezoneChange(newTimezone: string) {
const newDateWithTimezone = new TZDate(date.toISOString(), newTimezone); const newDateWithTimezone = new TZDate(date.toISOString(), newTimezone);
setTimezone(newTimezone);
setDate(newDateWithTimezone); setDate(newDateWithTimezone);
} }
@@ -80,6 +83,7 @@ function DateTimePicker({
if (!newOpenState) { if (!newOpenState) {
if (withTimezone) { if (withTimezone) {
const tz = defaultTimezone || guessTimezone(); const tz = defaultTimezone || guessTimezone();
setTimezone(tz);
setDate(new TZDate(dateTime, tz)); setDate(new TZDate(dateTime, tz));
} }
setDate(parseISO(dateTime)); setDate(parseISO(dateTime));
@@ -92,6 +96,8 @@ function DateTimePicker({
setOpen(false); setOpen(false);
} }
const selectedDateInUTC = new Date(date.getTime()).toISOString();
const dateString = formatDateFn?.(date) || format(date, 'PPP HH:mm:ss'); const dateString = formatDateFn?.(date) || format(date, 'PPP HH:mm:ss');
const errorText = validateDateFn?.(date); const errorText = validateDateFn?.(date);
@@ -101,6 +107,7 @@ function DateTimePicker({
<Popover open={open} onOpenChange={handleOpenChange}> <Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
data-testid="dateTimePickerTrigger"
variant="outline" variant="outline"
className={cn( className={cn(
'w-full justify-between text-left font-normal', 'w-full justify-between text-left font-normal',
@@ -113,15 +120,17 @@ function DateTimePicker({
<CalendarIcon className="h-4 w-4" /> <CalendarIcon className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align={align}> <PopoverContent className="w-auto p-0" align={align}>
<div className="flex"> <div className="flex">
<div className="flex"> <div className="flex">
<Calendar <Calendar
mode="single" mode="single"
selected={date} selected={date}
defaultMonth={date}
onSelect={(d) => handleSelect(d)} onSelect={(d) => handleSelect(d)}
initialFocus
disabled={isCalendarDayDisabled} disabled={isCalendarDayDisabled}
timeZone={timezone}
/> />
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<div> <div>
@@ -131,7 +140,7 @@ function DateTimePicker({
{withTimezone && ( {withTimezone && (
<div className="border-t border-border p-3"> <div className="border-t border-border p-3">
<TimezoneSettings <TimezoneSettings
dateTime={dateTime} dateTime={selectedDateInUTC}
onTimezoneChange={handleTimezoneChange} onTimezoneChange={handleTimezoneChange}
/> />
</div> </div>

View File

@@ -18,16 +18,23 @@ function TimezoneSettings({ dateTime, onTimezoneChange }: Props) {
setTimezone(tz.value); setTimezone(tz.value);
onTimezoneChange?.(tz.value); onTimezoneChange?.(tz.value);
} }
const utcOffset = getUTCOffsetInHours(selectedTimezone, dateTime, 'OOOO'); const utcOffset = getUTCOffsetInHours(selectedTimezone, dateTime, 'OOOO');
return ( return (
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
Timezone: {utcOffset}{' '} <span>Timezone: {utcOffset}</span>
<TimezonePicker <TimezonePicker
dateTime={dateTime} dateTime={dateTime}
selectedTimezone={selectedTimezone} selectedTimezone={selectedTimezone}
onTimezoneSelect={handleTimezoneSelect} onTimezoneSelect={handleTimezoneSelect}
button={ button={
<Button variant="ghost" size="icon"> <Button
variant="ghost"
size="icon"
aria-label="Open timezone settings"
data-testid="timezoneSettingsButton"
>
<Settings2 className="h-4 w-4 dark:text-foreground" /> <Settings2 className="h-4 w-4 dark:text-foreground" />
</Button> </Button>
} }

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

@@ -7,7 +7,6 @@ import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem'; import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs'; import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import {} from '@/utils/__generated__/graphql';
import { Divider } from '@mui/material'; import { Divider } from '@mui/material';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import Image from 'next/image'; import Image from 'next/image';

View File

@@ -1,7 +1,6 @@
import { render, screen } from '@/tests/orgs/testUtils'; import { render, screen, TestUserEvent } from '@/tests/testUtils';
import { guessTimezone } from '@/utils/timezoneUtils'; import { guessTimezone } from '@/utils/timezoneUtils';
import { TZDate } from '@date-fns/tz'; import { TZDate } from '@date-fns/tz';
import userEvent from '@testing-library/user-event';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { format } from 'date-fns-v4'; import { format } from 'date-fns-v4';
import { useState } from 'react'; import { useState } from 'react';
@@ -36,7 +35,7 @@ describe('TimePicker', () => {
expect(await screen.getByText(/Time:/i)).toHaveTextContent( expect(await screen.getByText(/Time:/i)).toHaveTextContent(
'Time: 03:00:05', 'Time: 03:00:05',
); );
const user = userEvent.setup(); const user = new TestUserEvent();
const hoursInput = await screen.getByLabelText('Hours'); const hoursInput = await screen.getByLabelText('Hours');
await user.type(hoursInput, '18'); await user.type(hoursInput, '18');
expect(await screen.getByText(/Time:/i)).toHaveTextContent( expect(await screen.getByText(/Time:/i)).toHaveTextContent(
@@ -46,7 +45,7 @@ describe('TimePicker', () => {
test('only valid hours(0-23), minutes(0-59) and seconds(0-59) are allowed', async () => { test('only valid hours(0-23), minutes(0-59) and seconds(0-59) are allowed', async () => {
render(<TestComponent dateTime="2025-03-10T03:00:05" />); render(<TestComponent dateTime="2025-03-10T03:00:05" />);
const user = userEvent.setup(); const user = new TestUserEvent();
const hoursInput = await screen.getByLabelText('Hours'); const hoursInput = await screen.getByLabelText('Hours');
await user.type(hoursInput, '30'); await user.type(hoursInput, '30');
expect(await screen.getByText(/Time:/i)).toHaveTextContent( expect(await screen.getByText(/Time:/i)).toHaveTextContent(
@@ -61,7 +60,7 @@ describe('TimePicker', () => {
test('Updates only the minutes of the date object', async () => { test('Updates only the minutes of the date object', async () => {
render(<TestComponent dateTime="2025-03-10T03:00:05" />); render(<TestComponent dateTime="2025-03-10T03:00:05" />);
const user = userEvent.setup(); const user = new TestUserEvent();
const minutesInput = await screen.getByLabelText('Minutes'); const minutesInput = await screen.getByLabelText('Minutes');
await user.type(minutesInput, '44'); await user.type(minutesInput, '44');
expect(await screen.getByText(/Time:/i)).toHaveTextContent( expect(await screen.getByText(/Time:/i)).toHaveTextContent(
@@ -71,7 +70,7 @@ describe('TimePicker', () => {
test('Updates only the seconds of the date object', async () => { test('Updates only the seconds of the date object', async () => {
render(<TestComponent dateTime="2025-03-10T03:00:05" />); render(<TestComponent dateTime="2025-03-10T03:00:05" />);
const user = userEvent.setup(); const user = new TestUserEvent();
const secondsInput = await screen.getByLabelText('Seconds'); const secondsInput = await screen.getByLabelText('Seconds');
await user.type(secondsInput, '11'); await user.type(secondsInput, '11');
expect(await screen.getByText(/Time:/i)).toHaveTextContent( expect(await screen.getByText(/Time:/i)).toHaveTextContent(
@@ -84,7 +83,7 @@ describe('TimePicker', () => {
expect(await screen.getByText(/Date class:/i)).toHaveTextContent( expect(await screen.getByText(/Date class:/i)).toHaveTextContent(
'Date class: TZDate', 'Date class: TZDate',
); );
const user = userEvent.setup(); const user = new TestUserEvent();
const hoursInput = await screen.getByLabelText('Hours'); const hoursInput = await screen.getByLabelText('Hours');
await user.type(hoursInput, '18'); await user.type(hoursInput, '18');

View File

@@ -3,12 +3,12 @@ import { Input } from '@/components/ui/v3/input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import React from 'react'; import React from 'react';
import { import {
type Period,
type TimePickerType,
copyDate, copyDate,
getArrowByType, getArrowByType,
getDateByType, getDateByType,
setDateByType, setDateByType,
type Period,
type TimePickerType,
} from './time-picker-utils'; } from './time-picker-utils';
export interface TimePickerInputProps export interface TimePickerInputProps

View File

@@ -229,7 +229,7 @@ export function getArrowByType(
} }
} }
function isTZDate(date: Date | TZDate): date is TZDate { export function isTZDate(date: Date | TZDate): date is TZDate {
return date instanceof TZDate; return date instanceof TZDate;
} }

View File

@@ -9,6 +9,26 @@ interface Props {
dateTime: string; dateTime: string;
} }
function getOrderedTimezones(dateTime: string, selectedTimezone: string) {
const [utcTimezone, browserTimezone, ...timezones] =
createTimezoneOptions(dateTime);
let orderedTimezones = [...timezones];
if (
selectedTimezone !== browserTimezone.value &&
selectedTimezone !== 'UTC'
) {
const selectedTimezoneOption = timezones.find(
(tz) => tz.value === selectedTimezone,
);
orderedTimezones = [
selectedTimezoneOption,
...timezones.filter((tz) => tz.value !== selectedTimezone),
];
}
return [utcTimezone, browserTimezone, ...orderedTimezones];
}
function TimezonePicker({ function TimezonePicker({
selectedTimezone, selectedTimezone,
onTimezoneSelect, onTimezoneSelect,
@@ -16,9 +36,10 @@ function TimezonePicker({
dateTime, dateTime,
}: Props) { }: Props) {
const timezoneOptions = useMemo( const timezoneOptions = useMemo(
() => createTimezoneOptions(dateTime), () => getOrderedTimezones(dateTime, selectedTimezone),
[dateTime], [dateTime, selectedTimezone],
); );
return ( return (
<VirtualizedCombobox <VirtualizedCombobox
options={timezoneOptions} options={timezoneOptions}
@@ -27,6 +48,7 @@ function TimezonePicker({
searchPlaceholder="Search timezones..." searchPlaceholder="Search timezones..."
button={button} button={button}
side="right" side="right"
width="370px"
/> />
); );
} }

View File

@@ -3,7 +3,7 @@ import { Box } from '@/components/ui/v2/Box';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon'; import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link'; import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog'; import { TransferOrUpgradeProjectDialog } from '@/features/orgs/components/common/TransferOrUpgradeProjectDialog';
import { useState } from 'react'; import { useState } from 'react';
import { OpenTransferDialogButton } from '@/components/common/OpenTransferDialogButton'; import { OpenTransferDialogButton } from '@/components/common/OpenTransferDialogButton';
@@ -51,7 +51,7 @@ export default function UpgradeToProBanner({
<div className="flex flex-col gap-2 space-y-2 lg:flex-row lg:items-center lg:space-x-2 lg:space-y-0"> <div className="flex flex-col gap-2 space-y-2 lg:flex-row lg:items-center lg:space-x-2 lg:space-y-0">
<OpenTransferDialogButton onClick={handleTransferDialogOpen} /> <OpenTransferDialogButton onClick={handleTransferDialogOpen} />
<TransferProjectDialog <TransferOrUpgradeProjectDialog
open={transferProjectDialogOpen} open={transferProjectDialogOpen}
setOpen={setTransferProjectDialogOpen} setOpen={setTransferProjectDialogOpen}
/> />

View File

@@ -105,20 +105,12 @@ function VirtualizedCommand<O extends Option>({
} }
}; };
React.useEffect(() => {
if (selectedOption) {
const option = filteredOptions.find(
(opt) => opt.value === selectedOption,
);
if (option) {
const index = filteredOptions.indexOf(option);
setFocusedIndex(index);
}
}
}, [selectedOption, filteredOptions, virtualizer]);
return ( return (
<Command shouldFilter={false} onKeyDown={handleKeyDown}> <Command
shouldFilter={false}
onKeyDown={handleKeyDown}
value={selectedOption}
>
<CommandInput onValueChange={handleSearch} placeholder={placeholder} /> <CommandInput onValueChange={handleSearch} placeholder={placeholder} />
<CommandList <CommandList
ref={parentRef} ref={parentRef}
@@ -145,7 +137,6 @@ function VirtualizedCommand<O extends Option>({
filteredOptions[virtualOption.index].key ?? filteredOptions[virtualOption.index].key ??
filteredOptions[virtualOption.index].value filteredOptions[virtualOption.index].value
} }
disabled={isKeyboardNavActive}
className={cn( className={cn(
'absolute left-0 top-0 w-full bg-transparent', 'absolute left-0 top-0 w-full bg-transparent',
focusedIndex === virtualOption.index && focusedIndex === virtualOption.index &&

View File

@@ -1,67 +0,0 @@
import { render, screen } from '@/tests/testUtils';
import type { Column } from 'react-table';
import { expect, test } from 'vitest';
import DataGrid from './DataGrid';
interface MockDataDetails {
id: number;
name: string;
}
const mockColumns: Column<MockDataDetails>[] = [
{ id: 'id', Header: 'ID', accessor: 'id' },
{ id: 'name', Header: 'Name', accessor: 'name' },
];
const mockData: MockDataDetails[] = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
];
test('should render an empty state if columns are not available', () => {
render(<DataGrid columns={[]} data={[]} />);
expect(screen.getByText(/columns not found/i)).toBeInTheDocument();
});
test('should render columns and empty state message if data is unavailable', () => {
render(<DataGrid columns={mockColumns} data={[]} />);
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: /id/i })).toBeInTheDocument();
expect(
screen.getByRole('columnheader', { name: /name/i }),
).toBeInTheDocument();
expect(screen.getByText(/no data is available/i)).toBeInTheDocument();
});
test('should render custom empty state message if data is unavailable', () => {
const customEmptyStateMessage = 'custom empty state message';
render(
<DataGrid
columns={mockColumns}
data={[]}
emptyStateMessage={customEmptyStateMessage}
/>,
);
expect(screen.getByText(customEmptyStateMessage)).toBeInTheDocument();
});
test('should display a loading indicator', async () => {
render(<DataGrid columns={mockColumns} data={[]} loading />);
// Activity indicator is not immediately displayed, so we need to wait
expect(await screen.findByRole('progressbar')).toBeInTheDocument();
});
test('should render data if provided', () => {
render(<DataGrid columns={mockColumns} data={mockData} />);
expect(screen.getAllByRole('row')).toHaveLength(2);
expect(screen.getByRole('cell', { name: /1/i })).toBeInTheDocument();
expect(screen.getByRole('cell', { name: /foo/i })).toBeInTheDocument();
});

View File

@@ -1,185 +0,0 @@
import type { UseDataGridOptions } from '@/components/dataGrid/DataGrid/useDataGrid';
import { DataGridBody } from '@/components/dataGrid/DataGridBody';
import { DataGridConfigProvider } from '@/components/dataGrid/DataGridConfigProvider';
import { DataGridFrame } from '@/components/dataGrid/DataGridFrame';
import type { DataGridHeaderProps } from '@/components/dataGrid/DataGridHeader';
import { DataGridHeader } from '@/components/dataGrid/DataGridHeader';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { DataBrowserEmptyState } from '@/features/database/dataGrid/components/DataBrowserEmptyState';
import type { DataBrowserGridColumn } from '@/features/database/dataGrid/types/dataBrowser';
import type { ForwardedRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import mergeRefs from 'react-merge-refs';
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
import { twMerge } from 'tailwind-merge';
import useDataGrid from './useDataGrid';
export interface DataGridProps<TColumnData extends object>
extends Omit<UseDataGridOptions<TColumnData>, 'tableRef'> {
/**
* Available columns.
*/
columns: Column<TColumnData>[];
/**
* Data to be displayed in the table.
*/
data: any[];
/**
* Text to be displayed when no data is available in the data grid.
*
* @default null
*/
emptyStateMessage?: string;
/**
* Additional configuration options for the `react-table` hook.
*/
options?: Omit<TableOptions<TColumnData>, 'columns' | 'data'>;
/**
* Additional data grid controls. This component will be part of the Data Grid
* context, so it can use Data Grid configuration.
*/
controls?:
| React.ReactNode
| ((selectedFlatRows: Row<TColumnData>[]) => React.ReactNode);
/**
* Function to be called when columns are sorted in the table.
*/
onSort?: (args: SortingRule<TColumnData>[]) => void;
/**
* Function to be called when the user wants to insert a new row.
*/
onInsertRow?: VoidFunction;
/**
* Function to be called when the user wants to insert a new column.
*/
onInsertColumn?: VoidFunction;
/**
* Function to be called when the user wants to remove a column.
*/
onRemoveColumn?: (column: DataBrowserGridColumn<TColumnData>) => void;
/**
* Function to be called when the user wants to edit a column.
*/
onEditColumn?: (column: DataBrowserGridColumn<TColumnData>) => void;
/**
* Determines whether or not data is loading.
*/
loading?: boolean;
/**
* Class name to be applied to the data grid.
*/
className?: string;
/**
* Sort configuration.
*/
sortBy?: SortingRule<TColumnData>[];
/**
* Props to be passed to the `DataGridHeader` component.
*/
headerProps?: DataGridHeaderProps<TColumnData>;
}
function DataGrid<TColumnData extends object>(
{
columns,
data,
allowSelection,
allowSort,
allowResize,
emptyStateMessage,
options = {},
headerProps,
controls,
sortBy,
onSort,
onInsertRow,
onInsertColumn,
onEditColumn,
onRemoveColumn,
loading,
className,
}: DataGridProps<TColumnData>,
ref: ForwardedRef<HTMLDivElement>,
) {
const tableRef = useRef<HTMLDivElement>();
const { toggleAllRowsSelected, setSortBy, ...dataGridProps } =
useDataGrid<TColumnData>({
columns: columns || [],
data: data || [],
allowSelection,
allowSort,
allowResize,
...options,
});
useEffect(() => {
if (!sortBy && setSortBy) {
setSortBy([]);
}
}, [setSortBy, sortBy]);
useEffect(() => {
if (onSort && allowSort) {
onSort(dataGridProps.state.sortBy);
if (toggleAllRowsSelected) {
toggleAllRowsSelected(false);
}
}
}, [allowSort, dataGridProps.state.sortBy, onSort, toggleAllRowsSelected]);
return (
<DataGridConfigProvider
toggleAllRowsSelected={toggleAllRowsSelected}
setSortBy={setSortBy}
tableRef={tableRef}
{...dataGridProps}
>
<>
{controls}
{columns.length === 0 && !loading && (
<DataBrowserEmptyState
title="Columns not found"
description="Please create a column before adding data to the table."
/>
)}
{columns.length > 0 && (
<Box
ref={mergeRefs([ref, tableRef])}
sx={{ backgroundColor: 'background.default' }}
className={twMerge(
'overflow-x-auto',
!loading && 'h-full',
className,
)}
>
<DataGridFrame>
<DataGridHeader
onInsertColumn={onInsertColumn}
onEditColumn={onEditColumn}
onRemoveColumn={onRemoveColumn}
{...headerProps}
/>
<DataGridBody
emptyStateMessage={emptyStateMessage}
loading={loading}
onInsertRow={onInsertRow}
allowInsertColumn={Boolean(onRemoveColumn)}
/>
</DataGridFrame>
</Box>
)}
{loading && <ActivityIndicator delay={1000} className="my-4" />}
</>
</DataGridConfigProvider>
);
}
export default forwardRef(DataGrid) as <TColumnData extends object>(
props: DataGridProps<TColumnData> & { ref?: ForwardedRef<HTMLDivElement> },
) => ReturnType<typeof DataGrid>;

View File

@@ -1,4 +0,0 @@
export * from './DataGrid';
export { default as DataGrid } from './DataGrid';
export * from './useDataGrid';
export { default as useDataGrid } from './useDataGrid';

View File

@@ -1,110 +0,0 @@
import { Checkbox } from '@/components/ui/v2/Checkbox';
import type { MutableRefObject } from 'react';
import { useMemo } from 'react';
import type { PluginHook, TableInstance, TableOptions } from 'react-table';
import {
useBlockLayout,
useResizeColumns,
useRowSelect,
useSortBy,
useTable,
} from 'react-table';
export interface UseDataGridBaseOptions {
/**
* Determines whether data grid columns are selectable.
*
* @default false
*/
allowSelection?: boolean;
/**
* Determines whether data grid columns are sortable.
*
* @default false
*/
allowSort?: boolean;
/**
* Determine whether data grid columns are resizable.
*
* @default false
*/
allowResize?: boolean;
/**
* Reference to the data grid root element.
*/
tableRef?: MutableRefObject<HTMLDivElement>;
}
export type UseDataGridOptions<T extends object = {}> = TableOptions<T> &
UseDataGridBaseOptions;
export type UseDataGridReturn<T extends object = {}> = TableInstance<T> &
UseDataGridBaseOptions;
export default function useDataGrid<T extends object>(
{ allowSelection, allowSort, allowResize, ...options }: UseDataGridOptions<T>,
...plugins: PluginHook<T>[]
): UseDataGridReturn<T> {
const defaultColumn = useMemo(
() => ({
width: 32,
minWidth: 32,
Cell: ({ value }: { value: any }) => (
<span className="truncate">
{typeof value === 'object' ? JSON.stringify(value) : value}
</span>
),
}),
[],
);
const pluginHooks = [
useBlockLayout,
useResizeColumns,
useSortBy,
useRowSelect,
];
const tableData = useTable<T>(
{
defaultColumn,
...options,
},
...pluginHooks,
...plugins,
(hooks) =>
allowSelection
? hooks.visibleColumns.push((columns) => [
{
id: 'selection',
Header: ({ rows, getToggleAllRowsSelectedProps }: any) => (
<Checkbox
disabled={rows.length === 0}
{...getToggleAllRowsSelectedProps({ style: null })}
style={{
...getToggleAllRowsSelectedProps().style,
cursor: rows.length === 0 ? 'default' : 'pointer',
}}
/>
),
Cell: ({ row }: any) => {
const originalValue = row.original as any;
return (
<Checkbox
{...row.getToggleRowSelectedProps()}
// disable selection if row is just a upload preview
checked={originalValue.uploading ? false : row.isSelected}
disabled={originalValue.uploading}
/>
);
},
disableSortBy: true,
disableResizing: true,
},
...columns,
])
: hooks.visibleColumns,
);
return { ...tableData, allowSort, allowResize, allowSelection };
}

View File

@@ -1,315 +0,0 @@
import type { DataGridProps } from '@/components/dataGrid/DataGrid';
import { DataGridCell } from '@/components/dataGrid/DataGridCell';
import { useDataGridConfig } from '@/components/dataGrid/DataGridConfigProvider';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import type { DataBrowserGridColumn } from '@/features/database/dataGrid/types/dataBrowser';
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
import { Fragment, useMemo, useRef } from 'react';
import type { Row } from 'react-table';
import { twMerge } from 'tailwind-merge';
export interface DataGridBodyProps<T extends object>
extends Omit<
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>,
'children'
>,
Pick<DataGridProps<T>, 'onInsertRow' | 'emptyStateMessage' | 'loading'> {
/**
* Determines whether column insertion is allowed.
*/
allowInsertColumn?: boolean;
}
interface InsertPlaceholderTableRowProps extends BoxProps {
/**
* Function to be called when the user wants to insert a new row.
*/
onInsertRow: VoidFunction;
}
function InsertPlaceholderTableRow({
onInsertRow,
...props
}: InsertPlaceholderTableRowProps) {
return (
<Box className="h-12 border-b-1 border-r-1" {...props}>
<Button
onClick={onInsertRow}
variant="borderless"
color="secondary"
className="h-full w-full justify-start rounded-none px-2 py-3 text-xs font-normal hover:shadow-none focus:shadow-none focus:outline-none"
startIcon={
<PlusIcon className="h-4 w-4" sx={{ color: 'text.secondary' }} />
}
>
Insert New Row
</Button>
</Box>
);
}
// TODO: Get rid of Data Browser related code from here. This component should
// be generic and not depend on Data Browser related data types and logic.
export default function DataGridBody<T extends object>({
emptyStateMessage = 'No data is available',
loading,
onInsertRow,
allowInsertColumn,
...props
}: DataGridBodyProps<T>) {
const { getTableBodyProps, totalColumnsWidth, rows, prepareRow, columns } =
useDataGridConfig<T>();
const SELECTION_CELL_WIDTH = 32;
const ADD_COLUMN_CELL_WIDTH = 100;
const bodyRef = useRef<HTMLDivElement>();
const primaryAndUniqueKeys = useMemo(
() =>
columns
.filter(
(column: DataBrowserGridColumn<T>) =>
column.isPrimary || column.isUnique,
)
.map((column) => column.id),
[columns],
);
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>, row: Row<T>) {
const { id: rowId } = row;
const cellId = document.activeElement.id;
const currentRow = bodyRef.current.children.namedItem(rowId);
if (event.key === 'ArrowUp') {
event.preventDefault();
if (!currentRow.previousElementSibling) {
return;
}
const cellInPreviousRow =
currentRow.previousElementSibling.children.namedItem(cellId);
if (cellInPreviousRow instanceof HTMLElement) {
cellInPreviousRow.scrollIntoView({
block: 'nearest',
});
cellInPreviousRow.focus();
}
}
if (event.key === 'ArrowDown') {
event.preventDefault();
if (!currentRow.nextElementSibling) {
return;
}
const cellInNextRow =
currentRow.nextElementSibling.children.namedItem(cellId);
if (cellInNextRow instanceof HTMLElement) {
cellInNextRow.scrollIntoView({ block: 'nearest' });
cellInNextRow.focus();
}
}
if (event.key === 'ArrowLeft' || (event.shiftKey && event.key === 'Tab')) {
let previousFocusableCellInRow: HTMLElement;
let previousFocusableCellInRowFound = false;
currentRow.childNodes.forEach((node) => {
if (node === currentRow.children.namedItem(cellId)) {
previousFocusableCellInRowFound = true;
}
if (
node instanceof HTMLElement &&
node.tabIndex > -1 &&
!previousFocusableCellInRowFound
) {
previousFocusableCellInRow = node;
}
});
if (previousFocusableCellInRow) {
event.preventDefault();
previousFocusableCellInRow.scrollIntoView({
block: 'nearest',
inline: 'center',
});
previousFocusableCellInRow.focus();
}
}
if (
event.key === 'ArrowRight' ||
(!event.shiftKey && event.key === 'Tab')
) {
let nextFocusableCellInRow: HTMLElement;
let nextFocusableCellInRowFound = false;
currentRow.childNodes.forEach((node) => {
if (
node instanceof HTMLElement &&
node.tabIndex > -1 &&
parseInt(node.id, 10) > parseInt(cellId, 10) &&
!nextFocusableCellInRowFound
) {
nextFocusableCellInRowFound = true;
nextFocusableCellInRow = node;
}
});
if (nextFocusableCellInRow) {
event.preventDefault();
nextFocusableCellInRow.scrollIntoView({
block: 'nearest',
inline: 'center',
});
nextFocusableCellInRow.focus();
}
}
}
const getBackgroundCellColor = (
row: Row<T>,
column: DataBrowserGridColumn<T>,
) => {
// Grey out files not uploaded
if (!row.values.isUploaded) {
return 'grey.200';
}
if (column.isDisabled) {
return 'grey.100';
}
return 'background.paper';
};
return (
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
{rows.length === 0 && !loading && (
<div className="flex flex-nowrap pr-5">
{onInsertRow ? (
<InsertPlaceholderTableRow
style={{
width: allowInsertColumn
? totalColumnsWidth + ADD_COLUMN_CELL_WIDTH
: totalColumnsWidth - SELECTION_CELL_WIDTH,
}}
onInsertRow={onInsertRow}
/>
) : (
<Box
className="inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs"
sx={{ color: 'text.secondary' }}
style={{
width: allowInsertColumn
? totalColumnsWidth + ADD_COLUMN_CELL_WIDTH
: totalColumnsWidth,
}}
>
{emptyStateMessage}
</Box>
)}
</div>
)}
{rows.map((row, index) => {
let rowKey = index.toString();
if (primaryAndUniqueKeys && primaryAndUniqueKeys.length > 0) {
rowKey = primaryAndUniqueKeys
.map((key) => row.values[key])
.filter(Boolean)
.join('-');
} else {
rowKey = `${index}-${Object.keys(row.values)
.map((key) => String(row.values[key]))
.join('-')}`;
}
prepareRow(row);
const rowProps = row.getRowProps({
style: {
width: allowInsertColumn
? totalColumnsWidth + ADD_COLUMN_CELL_WIDTH
: totalColumnsWidth,
},
});
return (
<Fragment key={rowKey.toString()}>
<div
{...rowProps}
id={row.id}
className="flex scroll-mt-10"
role="row"
onKeyDown={(event) => handleKeyDown(event, row)}
tabIndex={-1}
>
{row.cells.map((cell, cellIndex) => {
const column = cell.column as DataBrowserGridColumn<T>;
const isCellDisabled =
cell.value !== 0 &&
!cell.value &&
column.type !== 'boolean' &&
column.id !== 'selection' &&
column.isDisabled;
return (
<DataGridCell
{...cell.getCellProps({
style: {
display: 'inline-flex',
alignItems: 'center',
},
})}
cell={cell}
sx={{
backgroundColor: getBackgroundCellColor(row, column),
color: isCellDisabled ? 'text.secondary' : 'text.primary',
}}
className={twMerge(
'h-12 font-display text-xs motion-safe:transition-colors',
'border-b-1 border-r-1',
'scroll-ml-8 scroll-mt-[57px]',
column.id === 'selection' &&
'sticky left-0 z-20 justify-center px-0',
)}
isEditable={!column.isDisabled && column.isEditable}
id={cellIndex.toString()}
key={column.id}
>
{cell.render('Cell')}
</DataGridCell>
);
})}
{allowInsertColumn && (
<Box className="h-12 w-25 border-b-1 border-r-1" />
)}
</div>
{onInsertRow && index === rows.length - 1 && (
<InsertPlaceholderTableRow
{...rowProps}
key=""
onInsertRow={onInsertRow}
/>
)}
</Fragment>
);
})}
</div>
);
}

View File

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

View File

@@ -1,121 +0,0 @@
import type { CommonDataGridCellProps } from '@/components/dataGrid/DataGridCell';
import { useDataGridCell } from '@/components/dataGrid/DataGridCell';
import { ReadOnlyToggle } from '@/components/presentational/ReadOnlyToggle';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import type { MouseEvent, KeyboardEvent as ReactKeyboardEvent } from 'react';
import { twMerge } from 'tailwind-merge';
export type DataGridBooleanCellProps<TData extends object> =
CommonDataGridCellProps<TData, boolean | null>;
export default function DataGridBooleanCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
cell: {
column: { isNullable },
},
}: DataGridBooleanCellProps<TData>) {
const {
inputRef,
isEditing,
focusCell,
editCell,
cancelEditCell,
isSelected,
} = useDataGridCell<HTMLInputElement>();
async function handleMenuClick(
event: MouseEvent<HTMLLIElement> | ReactKeyboardEvent<HTMLLIElement>,
value: boolean | null,
) {
event.stopPropagation();
await onSave(value);
cancelEditCell();
}
async function handleMenuKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown'
) {
event.stopPropagation();
}
// We need to restore the temporary value, because editing was cancelled
if (event.key === 'Escape' && onTemporaryValueChange) {
event.stopPropagation();
onTemporaryValueChange(optimisticValue);
cancelEditCell();
}
if (event.key === 'Tab' && onSave) {
await onSave(temporaryValue);
cancelEditCell();
}
}
function handleTemporaryValueChange(value: boolean | null) {
if (onTemporaryValueChange) {
onTemporaryValueChange(value);
}
}
return isSelected ? (
<Dropdown.Root id="boolean-data-editor" className="h-full w-full">
<Dropdown.Trigger
id="boolean-trigger"
className={twMerge(
'h-full w-full border-none p-0 outline-none',
isEditing && 'p-1.5',
)}
ref={inputRef}
onClick={editCell}
autoFocus={false}
sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}
>
<ReadOnlyToggle checked={optimisticValue} />
</Dropdown.Trigger>
<Dropdown.Content
menu
disablePortal
onKeyDown={handleMenuKeyDown}
PaperProps={{ className: 'w-[200px]' }}
TransitionProps={{ onExited: focusCell }}
>
<Dropdown.Item
selected={optimisticValue === true}
onKeyUp={() => handleTemporaryValueChange(true)}
onClick={(event) => handleMenuClick(event, true)}
>
<ReadOnlyToggle checked />
</Dropdown.Item>
<Dropdown.Item
selected={optimisticValue === false}
onKeyUp={() => handleTemporaryValueChange(false)}
onClick={(event) => handleMenuClick(event, false)}
>
<ReadOnlyToggle checked={false} />
</Dropdown.Item>
{isNullable && (
<Dropdown.Item
selected={optimisticValue === null}
onKeyUp={() => handleTemporaryValueChange(null)}
onClick={(event) => handleMenuClick(event, null)}
>
<ReadOnlyToggle checked={null} />
</Dropdown.Item>
)}
</Dropdown.Content>
</Dropdown.Root>
) : (
<ReadOnlyToggle checked={optimisticValue} />
);
}

View File

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

View File

@@ -1,381 +0,0 @@
import { useDialog } from '@/components/common/DialogProvider';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Tooltip, useTooltip } from '@/components/ui/v2/Tooltip';
import type {
ColumnType,
DataBrowserGridCell,
DataBrowserGridCellProps,
} from '@/features/database/dataGrid/types/dataBrowser';
import { triggerToast } from '@/utils/toast';
import type {
FocusEvent,
JSXElementConstructor,
KeyboardEvent,
MouseEvent,
ReactElement,
ReactNode,
ReactPortal,
} from 'react';
import {
Children,
cloneElement,
isValidElement,
useEffect,
useState,
} from 'react';
import { twMerge } from 'tailwind-merge';
import DataGridCellProvider from './DataGridCellProvider';
import useDataGridCell from './useDataGridCell';
export interface CommonDataGridCellProps<TData extends object, TValue = any>
extends DataBrowserGridCellProps<TData, TValue> {
/**
* Function that is called when the cell is saved.
*/
onSave?: (value: TValue, options?: { reset: boolean }) => Promise<void>;
/**
* Optimistic value for the cell.
*/
optimisticValue?: TValue;
/**
* Function to be called when the optimistic value should be changed.
*/
onOptimisticValueChange?: (value: TValue) => void;
/**
* Temporary value for the cell. This is used for storing the current input
* value, that should be later saved as an optimistic value before saving the
* data.
*/
temporaryValue?: TValue;
/**
* Function to be called when the temporary value should be changed.
*/
onTemporaryValueChange?: (value: TValue) => void;
}
export interface DataGridCellProps<TData extends object, TValue = unknown>
extends BoxProps {
/**
* Current cell's props.
*/
cell: DataBrowserGridCell<TData, TValue>;
/**
* Determines whether the cell is editable.
*/
isEditable?: boolean;
/**
* Determines the column's type.
*/
columnType?: ColumnType;
}
function DataGridCellContent<TData extends object = {}, TValue = unknown>({
isEditable,
children,
className,
cell: {
value: originalValue,
column: { onCellEdit, id, isNullable, isPrimary, type },
row,
},
...props
}: DataGridCellProps<TData, TValue>) {
const { openAlertDialog } = useDialog();
const {
title: tooltipTitle,
open: tooltipOpen,
openTooltip,
closeTooltip,
resetTooltipTitle,
} = useTooltip();
const [optimisticValue, setOptimisticValue] = useState<TValue>(originalValue);
const [temporaryValue, setTemporaryValue] = useState<TValue>(originalValue);
useEffect(() => {
setOptimisticValue(originalValue);
setTemporaryValue(originalValue);
}, [originalValue]);
const {
cellRef,
inputRef,
focusCell,
focusInput,
blurInput,
clickInput,
isEditing,
isSelected,
selectCell,
deselectCell,
cancelEditCell,
editCell,
focusPrevCell,
focusNextCell,
} = useDataGridCell();
function activateInput() {
if (isPrimary) {
openTooltip("Primary keys can't be edited.");
return;
}
editCell();
if (type === 'boolean') {
clickInput();
} else {
focusInput();
}
}
async function handleClick(event: MouseEvent<HTMLDivElement>) {
if (!isEditable || isEditing || isPrimary) {
return;
}
if (event.detail === 2 && type !== 'boolean') {
editCell();
await focusInput();
}
}
function handleFocus() {
if (!isEditable) {
return;
}
selectCell();
}
async function handleSave(
value: TValue,
options: { reset: boolean } = { reset: false },
) {
if (!onCellEdit) {
return;
}
const normalizedValue =
value !== null && typeof value === 'object'
? JSON.stringify(value)
: String(value);
const normalizedOptimisticValue =
optimisticValue !== null && typeof optimisticValue === 'object'
? JSON.stringify(optimisticValue)
: String(optimisticValue);
// We are making sure that optimistic value is not equal to the current
// value. If it is, we are not going to save the value.
if (
normalizedValue.replace(/\n/gi, '\\n') ===
normalizedOptimisticValue.replace(/\n/gi, '\\n') &&
!options.reset
) {
return;
}
// In case of an error, we need to reset optimistic value
const latestOptimisticValue = optimisticValue;
setOptimisticValue(value);
try {
const data = await onCellEdit({
row,
columnsToUpdate: {
[id]: {
value: !options.reset ? value : undefined,
reset: options.reset,
},
},
});
// Syncing optimistic value with server-side value
setTemporaryValue(data.original[id.toString()]);
setOptimisticValue(data.original[id.toString()]);
} catch (error) {
triggerToast(`Error: ${error.message || 'Unknown error occurred.'}`);
// Resetting values
setTemporaryValue(latestOptimisticValue);
setOptimisticValue(latestOptimisticValue);
}
}
async function handleBlur(event: FocusEvent<HTMLDivElement>) {
// We are deselecting cell only if focus target is not a descendant of it.
if (!isEditable || event.currentTarget.contains(event.relatedTarget)) {
return;
}
await handleSave(temporaryValue);
closeTooltip();
deselectCell();
}
function resetCell() {
if (isPrimary) {
openTooltip('Primary keys are non-nullable.');
return;
}
if (!isNullable) {
openTooltip(
<span>
<strong>{id}</strong>
is non-nullable.
</span>,
);
return;
}
openAlertDialog({
title: 'Set value to null',
payload: (
<p>
Are you sure you want to set this cell to <strong>null</strong>?
</p>
),
props: {
primaryButtonText: 'Set to null',
primaryButtonColor: 'error',
onPrimaryAction: async () => {
await handleSave(null, { reset: true });
focusCell();
},
},
});
}
async function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
if (!isEditable) {
return;
}
if (event.key === 'Escape') {
closeTooltip();
}
// Resetting temporary value and focusing cell on Escape when input field is
// focused
if (event.key === 'Escape' && event.target === inputRef.current) {
setTemporaryValue(optimisticValue);
await focusCell();
cancelEditCell();
}
// Activating input field on Enter
if (event.key === 'Enter' && event.target === cellRef.current) {
activateInput();
}
// Focusing next cell on Tab
if (event.key === 'Tab' && !event.shiftKey) {
event.stopPropagation();
const nextCellAvailable = focusNextCell();
if (!nextCellAvailable) {
event.preventDefault();
event.stopPropagation();
await blurInput();
await focusCell();
}
}
// Focusing previous cell on Shift-Tab
if (event.key === 'Tab' && event.shiftKey) {
event.stopPropagation();
const prevCellAvailable = focusPrevCell();
if (!prevCellAvailable) {
event.preventDefault();
event.stopPropagation();
await blurInput();
await focusCell();
}
}
// Initiating cell reset when cell is focused
if (event.key === 'Backspace' && event.target === cellRef.current) {
resetCell();
}
}
const content = (
<Box
ref={cellRef}
className={twMerge(
'relative grid h-full w-full cursor-default grid-flow-col items-center gap-1',
isEditable &&
'focus-within:outline-none focus-within:ring-0 focus:ring-0',
isSelected && 'shadow-outline',
isEditing ? 'p-0.5 shadow-outline-dark' : 'px-2 py-1.5',
className,
)}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
tabIndex={isEditable ? 0 : undefined}
onClick={handleClick}
role="textbox"
sx={{ backgroundColor: 'transparent' }}
{...props}
>
{Children.map(
children,
(
child:
| ReactNode
| ReactPortal
| ReactElement<unknown, string | JSXElementConstructor<any>>,
) => {
if (!isValidElement(child)) {
return null;
}
return cloneElement(child, {
...child.props,
onSave: handleSave,
optimisticValue,
onOptimisticValueChange: setOptimisticValue,
temporaryValue,
onTemporaryValueChange: setTemporaryValue,
});
},
)}
</Box>
);
if (isEditable) {
return (
<Tooltip
disableHoverListener
disableFocusListener
open={tooltipOpen}
title={tooltipTitle || ''}
TransitionProps={{ onExited: resetTooltipTitle }}
>
{content}
</Tooltip>
);
}
return content;
}
export default function DataGridCell<TData extends object, TValue = unknown>(
props: DataGridCellProps<TData, TValue>,
) {
return (
<DataGridCellProvider>
<DataGridCellContent {...props} />
</DataGridCellProvider>
);
}

View File

@@ -1,238 +0,0 @@
import type { MutableRefObject, PropsWithChildren } from 'react';
import { createContext, useCallback, useMemo, useReducer, useRef } from 'react';
export interface DataGridCellContextProps<T extends HTMLElement> {
/**
* This `ref` should be attached to the cell element.
*/
cellRef: MutableRefObject<HTMLDivElement>;
/**
* This `ref` should be attached to the input element inside the data grid cell.
*/
inputRef: MutableRefObject<T>;
/**
* Determines whether or not the cell is currently being edited.
*/
isEditing: boolean;
/**
* Determines whether or not the cell is currently selected.
*/
isSelected: boolean;
/**
* Function to be called to start editing.
*/
editCell: VoidFunction;
/**
* Function to be called to cancel editing.
*/
cancelEditCell: VoidFunction;
/**
* Function to be called to select the cell, but not start editing.
*/
selectCell: VoidFunction;
/**
* Function to be called to deselect cell and cancel editing.
*/
deselectCell: VoidFunction;
/**
* Function to be called to focus cell.
*/
focusCell: () => Promise<void>;
/**
* Function to be called to blur cell.
*/
blurCell: () => Promise<void>;
/**
* Function to be called to programatically focus the input in the cell.
*/
focusInput: () => Promise<void>;
/**
* Function to be called to programatically blur the input in the cell.
*/
blurInput: () => Promise<void>;
/**
* Function to be called to programmatically click the input in the cell.
*/
clickInput: () => Promise<void>;
/**
* Function to be called to navigate to next cell if available.
*
* @returns `true` if there is a next cell to focus, `false` otherwise.
*/
focusNextCell: () => boolean;
/**
* Function to be called to navigate to previous cell if available.
*
* @returns `true` if there is a previous cell to focus, `false` otherwise.
*/
focusPrevCell: () => boolean;
}
export const DataGridCellContext =
createContext<DataGridCellContextProps<any>>(null);
interface EditAndSelectState {
isEditing: boolean;
isSelected: boolean;
}
type EditAndSelectAction =
| { type: 'EDIT' }
| { type: 'CANCEL_EDIT' }
| { type: 'SELECT' }
| { type: 'DESELECT' };
function editAndSelectCellReducer(
state: EditAndSelectState,
action: EditAndSelectAction,
): EditAndSelectState {
switch (action.type) {
case 'EDIT':
return { ...state, isEditing: true, isSelected: true };
case 'CANCEL_EDIT':
return { ...state, isEditing: false };
case 'SELECT':
return { ...state, isSelected: true };
case 'DESELECT':
return { ...state, isEditing: false, isSelected: false };
default:
return { ...state };
}
}
export default function DataGridCellProvider<TInput extends HTMLElement>({
children,
}: PropsWithChildren<unknown>) {
const cellRef = useRef<HTMLDivElement>();
const inputRef = useRef<TInput>();
const [{ isEditing, isSelected }, dispatch] = useReducer(
editAndSelectCellReducer,
{
isEditing: false,
isSelected: false,
},
);
function focusCell() {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
cellRef.current?.focus();
resolve();
});
});
}
function deselectCell() {
dispatch({ type: 'DESELECT' });
}
const focusPrevCell = useCallback(() => {
const prevCellAvailable =
cellRef.current.previousElementSibling instanceof HTMLElement &&
cellRef.current.previousElementSibling.tabIndex > -1;
requestAnimationFrame(() => {
if (prevCellAvailable) {
(cellRef.current.previousElementSibling as HTMLElement).focus();
deselectCell();
}
});
return prevCellAvailable;
}, []);
const focusNextCell = useCallback(() => {
const nextCellAvailable =
cellRef.current.nextElementSibling instanceof HTMLElement &&
cellRef.current.nextElementSibling.tabIndex > -1;
requestAnimationFrame(() => {
if (nextCellAvailable) {
(cellRef.current.nextElementSibling as HTMLElement).focus();
deselectCell();
}
});
return nextCellAvailable;
}, []);
function blurCell() {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
cellRef.current?.blur();
resolve();
});
});
}
function focusInput() {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
inputRef.current?.focus();
resolve();
});
});
}
function blurInput() {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
inputRef.current?.blur();
resolve();
});
});
}
function clickInput() {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => {
inputRef.current?.click();
resolve();
});
});
}
function editCell() {
dispatch({ type: 'EDIT' });
}
function cancelEditCell() {
dispatch({ type: 'CANCEL_EDIT' });
}
function selectCell() {
dispatch({ type: 'SELECT' });
}
const value = useMemo(
() => ({
focusCell,
blurCell,
focusInput,
blurInput,
clickInput,
isEditing,
isSelected,
editCell,
cancelEditCell,
selectCell,
deselectCell,
cellRef,
inputRef,
focusPrevCell,
focusNextCell,
}),
[focusNextCell, focusPrevCell, isEditing, isSelected],
);
return (
<DataGridCellContext.Provider value={value}>
{children}
</DataGridCellContext.Provider>
);
}

View File

@@ -1,5 +0,0 @@
export * from './DataGridCell';
export { default as DataGridCell } from './DataGridCell';
export * from './DataGridCellProvider';
export { default as DataGridCellProvider } from './DataGridCellProvider';
export { default as useDataGridCell } from './useDataGridCell';

View File

@@ -1,10 +0,0 @@
import { useContext } from 'react';
import type { DataGridCellContextProps } from './DataGridCellProvider';
import { DataGridCellContext } from './DataGridCellProvider';
export default function useDataGridCell<TInput extends HTMLElement>() {
const context =
useContext<DataGridCellContextProps<TInput>>(DataGridCellContext);
return context;
}

View File

@@ -1,6 +0,0 @@
import type { UseDataGridReturn } from '@/components/dataGrid/DataGrid';
import { createContext } from 'react';
const DataGridConfigContext = createContext<Partial<UseDataGridReturn>>(null);
export default DataGridConfigContext;

View File

@@ -1,16 +0,0 @@
import type { UseDataGridReturn } from '@/components/dataGrid/DataGrid';
import type { PropsWithChildren } from 'react';
import DataGridConfigContext from './DataGridConfigContext';
export default function DataGridConfigProvider<T extends object = {}>({
children,
...value
}: PropsWithChildren<UseDataGridReturn<T>>) {
return (
<DataGridConfigContext.Provider
value={value as unknown as UseDataGridReturn<{}>}
>
{children}
</DataGridConfigContext.Provider>
);
}

View File

@@ -1,3 +0,0 @@
export { default as DataGridConfigContext } from './DataGridConfigContext';
export { default as DataGridConfigProvider } from './DataGridConfigProvider';
export { default as useDataGridConfig } from './useDataGridConfig';

View File

@@ -1,15 +0,0 @@
import type { UseDataGridReturn } from '@/components/dataGrid/DataGrid';
import { useContext } from 'react';
import DataGridConfigContext from './DataGridConfigContext';
export default function useDataGridConfig<T extends object = {}>() {
const context = useContext(DataGridConfigContext);
if (!context) {
throw new Error(
`useDataGridConfig must be used within a DataGridConfigContext`,
);
}
return context as unknown as UseDataGridReturn<T>;
}

View File

@@ -1,166 +0,0 @@
import type { CommonDataGridCellProps } from '@/components/dataGrid/DataGridCell';
import { useDataGridCell } from '@/components/dataGrid/DataGridCell';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import type { TextProps } from '@/components/ui/v2/Text';
import { Text } from '@/components/ui/v2/Text';
import { getDateComponents } from '@/utils/getDateComponents';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { twMerge } from 'tailwind-merge';
export interface DataGridDateCellProps<TData extends object>
extends CommonDataGridCellProps<TData, string> {
/**
* Props to be passed to date display.
*/
dateProps?: TextProps;
/**
* Props to be passed to time display.
*/
timeProps?: TextProps;
}
export default function DataGridDateCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
cell: {
column: { specificType },
},
dateProps,
timeProps,
className,
}: DataGridDateCellProps<TData>) {
const { className: dateClassName, ...restDateProps } = dateProps || {};
const { className: timeClassName, ...restTimeProps } = timeProps || {};
// Note: No date (year-month-day) is saved for time / timetz columns, so we
// need to add it manually.
const date =
optimisticValue && specificType !== 'interval'
? new Date(
specificType === 'time' || specificType === 'timetz'
? `1970-01-01 ${optimisticValue}`
: optimisticValue,
)
: undefined;
const { year, month, day, hour, minute, second } = getDateComponents(date, {
adjustTimezone: ['date', 'timetz', 'timestamptz'].includes(specificType),
});
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
await onSave(temporaryValue || '');
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (event.target instanceof HTMLInputElement && onTemporaryValueChange) {
onTemporaryValueChange(event.target.value);
}
}
if (isEditing) {
return (
<Input
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (!optimisticValue) {
return (
<Text className="truncate text-xs" color="secondary">
null
</Text>
);
}
if (specificType === 'interval') {
return <Text className="truncate text-xs">{optimisticValue}</Text>;
}
return (
<div className={twMerge('grid grid-flow-row', className)}>
{specificType !== 'time' && specificType !== 'timetz' && (
<Text
className={twMerge('truncate text-xs', dateClassName)}
{...restDateProps}
>
{[year, month, day].filter(Boolean).join('-')}
</Text>
)}
{specificType !== 'date' && (
<Text
className={twMerge('truncate text-xs', timeClassName)}
color={
specificType === 'time' || specificType === 'timetz'
? 'primary'
: 'secondary'
}
{...restTimeProps}
>
{[hour, minute, second].filter(Boolean).join(':')}
</Text>
)}
</div>
);
}

View File

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

View File

@@ -1,29 +0,0 @@
import { useDataGridConfig } from '@/components/dataGrid/DataGridConfigProvider';
import clsx from 'clsx';
import type { DetailedHTMLProps, HTMLProps } from 'react';
export type DataGridFrameProps = DetailedHTMLProps<
HTMLProps<HTMLDivElement>,
HTMLDivElement
>;
export default function DataGridFrame<T extends object>({
style,
children,
className,
...props
}: DataGridFrameProps) {
const { getTableProps } = useDataGridConfig<T>();
const { style: reactTableStyle, ...restTableProps } = getTableProps();
return (
<div
{...restTableProps}
{...props}
className={clsx('min-w-min', className)}
style={{ ...reactTableStyle, minWidth: undefined, ...style }}
>
{children}
</div>
);
}

View File

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

View File

@@ -1,233 +0,0 @@
import type { DataGridProps } from '@/components/dataGrid/DataGrid';
import { useDataGridConfig } from '@/components/dataGrid/DataGridConfigProvider';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { ArrowDownIcon } from '@/components/ui/v2/icons/ArrowDownIcon';
import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon';
import { PencilIcon } from '@/components/ui/v2/icons/PencilIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import type { DataBrowserGridColumn } from '@/features/database/dataGrid/types/dataBrowser';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { twMerge } from 'tailwind-merge';
export interface HeaderActionProps
extends DetailedHTMLProps<HTMLProps<HTMLElement>, HTMLElement> {}
export interface DataGridHeaderProps<T extends object>
extends Omit<
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>,
'children'
>,
Pick<
DataGridProps<T>,
'onRemoveColumn' | 'onEditColumn' | 'onInsertColumn'
> {
/**
* Props to be passed to component slots.
*/
componentsProps?: {
/**
* Props to be passed to the `Edit Column` header action item.
*/
editActionProps?: HeaderActionProps;
/**
* Props to be passed to the `Delete Column` header action item.
*/
deleteActionProps?: HeaderActionProps;
/**
* Props to be passed to the `Delete Column` header action item.
*/
insertActionProps?: HeaderActionProps;
};
}
// TODO: Get rid of Data Browser related code from here. This component should
// be generic and not depend on Data Browser related data types and logic.
export default function DataGridHeader<T extends object>({
className,
onRemoveColumn,
onEditColumn,
onInsertColumn,
componentsProps,
...props
}: DataGridHeaderProps<T>) {
const { flatHeaders, allowSort, allowResize } = useDataGridConfig<T>();
return (
<div
className={twMerge(
'sticky top-0 z-30 inline-flex w-full items-center pr-5',
className,
)}
{...props}
>
{flatHeaders.map((column: DataBrowserGridColumn<T>) => {
const headerProps = column.getHeaderProps({
style: { display: 'inline-grid' },
});
return (
<Dropdown.Root
sx={{
backgroundColor: (theme) =>
column.isDisabled
? theme.palette.background.default
: theme.palette.background.paper,
color: 'text.primary',
borderColor: 'grey.300',
}}
className={twMerge(
'group relative inline-flex self-stretch overflow-hidden font-display text-xs font-bold focus:outline-none focus-visible:outline-none',
'border-b-1 border-r-1',
column.id === 'selection' && 'sticky left-0 max-w-2',
)}
style={{
...headerProps.style,
maxWidth:
column.id === 'selection' ? 32 : headerProps.style?.maxWidth,
width:
column.id === 'selection' ? '100%' : headerProps.style?.width,
zIndex:
column.id === 'selection' ? 10 : headerProps.style?.zIndex,
position: null,
}}
key={column.id}
>
{column.id === 'selection' ? (
<span
{...headerProps}
className="relative grid w-full grid-flow-col items-center justify-between p-2"
>
{column.render('Header')}
</span>
) : (
<Dropdown.Trigger
className={twMerge(
'focus:outline-none motion-safe:transition-colors',
)}
disabled={
column.isDisabled || (column.disableSortBy && !onRemoveColumn)
}
hideChevron
>
<span
{...headerProps}
className="relative grid w-full grid-flow-col items-center justify-between p-2"
>
{column.render('Header')}
{allowSort && (
<Box component="span" sx={{ color: 'text.primary' }}>
{column.isSorted && !column.isSortedDesc && (
<ArrowUpIcon className="h-3 w-3" />
)}
{column.isSorted && column.isSortedDesc && (
<ArrowDownIcon className="h-3 w-3" />
)}
</Box>
)}
</span>
{allowResize && !column.disableResizing && (
<span
{...column.getResizerProps({
onClick: (event: Event) => event.stopPropagation(),
})}
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
/>
)}
</Dropdown.Trigger>
)}
<Dropdown.Content
menu
PaperProps={{ className: 'w-52 mt-1' }}
className="p-0"
>
{onEditColumn && (
<Dropdown.Item
onClick={() => onEditColumn(column)}
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
disabled={componentsProps?.editActionProps?.disabled}
>
<PencilIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Edit Column</span>
</Dropdown.Item>
)}
{onEditColumn && <Divider component="li" sx={{ margin: 0 }} />}
{!column.disableSortBy && (
<Dropdown.Item
onClick={() => column.toggleSortBy(false)}
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<ArrowUpIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Sort Ascending</span>
</Dropdown.Item>
)}
{!column.disableSortBy && (
<Dropdown.Item
onClick={() => column.toggleSortBy(true)}
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<ArrowDownIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>Sort Descending</span>
</Dropdown.Item>
)}
{onRemoveColumn && !column.isPrimary && (
<Divider component="li" className="my-1" />
)}
{onRemoveColumn && !column.isPrimary && (
<Dropdown.Item
onClick={() => onRemoveColumn(column)}
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
disabled={componentsProps?.deleteActionProps?.disabled}
sx={{ color: 'error.main' }}
>
<TrashIcon className="h-4 w-4" sx={{ color: 'error.main' }} />
<span>Delete Column</span>
</Dropdown.Item>
)}
</Dropdown.Content>
</Dropdown.Root>
);
})}
{onInsertColumn && (
<Box className="group relative inline-flex w-25 self-stretch overflow-hidden border-b-1 border-r-1 font-display text-xs font-bold focus:outline-none focus-visible:outline-none">
<Button
onClick={onInsertColumn}
variant="borderless"
color="secondary"
className="h-full w-full rounded-none text-xs hover:shadow-none focus:shadow-none focus:outline-none"
aria-label="Insert New Column"
disabled={componentsProps?.insertActionProps?.disabled}
>
<PlusIcon className="h-4 w-4" sx={{ color: 'text.disabled' }} />
</Button>
</Box>
)}
</div>
);
}

View File

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

View File

@@ -1,110 +0,0 @@
import type { CommonDataGridCellProps } from '@/components/dataGrid/DataGridCell';
import { useDataGridCell } from '@/components/dataGrid/DataGridCell';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import type { ChangeEvent, KeyboardEvent } from 'react';
export type DataGridNumericCellProps<TData extends object> =
CommonDataGridCellProps<TData, number>;
export default function DataGridNumericCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
}: DataGridNumericCellProps<TData>) {
const { inputRef, focusCell, isEditing, cancelEditCell } =
useDataGridCell<HTMLInputElement>();
async function handleSave() {
if (onSave) {
if (typeof temporaryValue === 'number') {
await onSave(temporaryValue);
} else {
await onSave(null);
}
}
}
async function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
if (onTemporaryValueChange) {
if (event.target.value) {
onTemporaryValueChange(parseInt(event.target.value, 10));
} else {
onTemporaryValueChange(null);
}
}
}
if (isEditing) {
return (
<Input
type="number"
ref={inputRef}
value={
temporaryValue !== null && typeof temporaryValue !== 'undefined'
? temporaryValue
: ''
}
onKeyDown={handleKeyDown}
onChange={handleChange}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (optimisticValue === null || typeof optimisticValue === 'undefined') {
return (
<Text className="truncate !text-xs" color="disabled">
null
</Text>
);
}
return <Text className="truncate !text-xs">{optimisticValue}</Text>;
}

View File

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

View File

@@ -1,91 +0,0 @@
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import type { IconButtonProps } from '@/components/ui/v2/IconButton';
import { IconButton } from '@/components/ui/v2/IconButton';
import { ChevronLeftIcon } from '@/components/ui/v2/icons/ChevronLeftIcon';
import { ChevronRightIcon } from '@/components/ui/v2/icons/ChevronRightIcon';
import { Text } from '@/components/ui/v2/Text';
import clsx from 'clsx';
export interface DataGridPaginationProps extends BoxProps {
/**
* Number of pages.
*/
totalPages: number;
/**
* Current page.
*/
currentPage: number;
/**
* Function to be called when navigating to the previous page.
*/
onOpenPrevPage: VoidFunction;
/**
* Function to be called when navigating to the next page.
*/
onOpenNextPage: VoidFunction;
/**
* Props to be passed to the next button component.
*/
nextButtonProps?: IconButtonProps;
/**
* Props to be passed to the previous button component.
*/
prevButtonProps?: IconButtonProps;
}
export default function DataGridPagination({
className,
totalPages,
currentPage,
onOpenPrevPage,
onOpenNextPage,
nextButtonProps,
prevButtonProps,
...props
}: DataGridPaginationProps) {
return (
<Box
className={clsx(
'grid grid-flow-col items-center justify-around rounded-md border-1',
className,
)}
{...props}
>
<IconButton
variant="borderless"
color="secondary"
disabled={currentPage === 1}
onClick={onOpenPrevPage}
aria-label="Previous page"
{...prevButtonProps}
>
<ChevronLeftIcon className="h-4 w-4" />
</IconButton>
<span
className={clsx(
'mx-1 inline-block font-display font-medium',
currentPage > 99 ? 'text-xs' : 'text-sm+',
)}
>
{currentPage}
<Text component="span" className="mx-1 inline-block" color="disabled">
/
</Text>
{totalPages}
</span>
<IconButton
variant="borderless"
color="secondary"
disabled={currentPage === totalPages}
onClick={onOpenNextPage}
aria-label="Next page"
{...nextButtonProps}
>
<ChevronRightIcon className="h-4 w-4" />
</IconButton>
</Box>
);
}

View File

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

View File

@@ -1,410 +0,0 @@
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { AudioPreviewIcon } from '@/components/ui/v2/icons/AudioPreviewIcon';
import { FilePreviewIcon } from '@/components/ui/v2/icons/FilePreviewIcon';
import { PDFPreviewIcon } from '@/components/ui/v2/icons/PDFPreviewIcon';
import { VideoPreviewIcon } from '@/components/ui/v2/icons/VideoPreviewIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { useAppClient } from '@/features/projects/common/hooks/useAppClient';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { useEffect, useReducer, useState } from 'react';
import type { CellProps } from 'react-table';
export type PreviewProps = {
fetchBlob: (
init?: RequestInit,
size?: { width?: number; height?: number },
) => Promise<Blob | null>;
mimeType?: string;
alt?: string;
blob?: Blob;
id?: string;
};
export type DataGridPreviewCellProps<TData extends object> = CellProps<
TData,
PreviewProps
> & {
/**
* Preview to use when the file is not an image or blob can't be fetched
* properly.
*
* @default null
*/
fallbackPreview?: ReactNode;
};
function useBlob({
fetchBlob,
blob,
mimeType,
}: Pick<PreviewProps, 'fetchBlob' | 'blob' | 'mimeType'>) {
const [objectUrl, setObjectUrl] = useState<string>();
const [error, setError] = useState<Error>();
const [loading, setLoading] = useState<boolean>(false);
// This side-effect fetches the blob of the file from the server and sets the
// relevant `objectUrl` state. Abort controller is reponsible for cancelling
// the fetch if the component is unmounted.
useEffect(() => {
const abortController = new AbortController();
async function generateOptimizedObjectUrl() {
// todo: it could be more declarative if this function was called with the
// actual preview URL here, not pre-generated in useFiles
const fetchedBlob = await fetchBlob(
{ signal: abortController.signal },
mimeType !== 'image/svg+xml' && { width: 80, height: 40 },
);
if (fetchedBlob) {
return URL.createObjectURL(fetchedBlob);
}
return undefined;
}
async function generateObjectUrl() {
setLoading(false);
setError(undefined);
if (objectUrl || (mimeType && !mimeType?.startsWith('image'))) {
return;
}
if (blob) {
setObjectUrl(URL.createObjectURL(blob));
return;
}
try {
setLoading(true);
const generatedObjectUrl = await generateOptimizedObjectUrl();
if (!abortController.signal.aborted) {
setObjectUrl(generatedObjectUrl);
}
} catch (generateError) {
if (!abortController.signal.aborted) {
setError(generateError);
}
}
if (!abortController.signal.aborted) {
setLoading(false);
}
}
generateObjectUrl();
return () => abortController.abort();
}, [blob, fetchBlob, objectUrl, mimeType]);
return { objectUrl, error, loading };
}
const previewableImages = [
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
];
const previewableVideos = [
'video/mp4',
'video/x-m4v',
'video/3gpp',
'video/3gpp2',
];
const previewableFileTypes = [
...previewableImages,
...previewableVideos,
'audio/',
'application/json',
];
function previewReducer(
state: { loading: boolean; error?: Error; data?: string },
action:
| { type: 'PREVIEW_LOADING' }
| { type: 'CLEAR_PREVIEW' }
| { type: 'PREVIEW_FETCHED'; payload: string }
| { type: 'PREVIEW_ERROR'; payload: Error },
): { loading: boolean; error?: Error; data?: string } {
switch (action.type) {
case 'PREVIEW_LOADING':
return { ...state, loading: true, error: undefined, data: undefined };
case 'PREVIEW_FETCHED':
return {
...state,
loading: false,
error: undefined,
data: action.payload,
};
case 'PREVIEW_ERROR':
return {
...state,
loading: false,
error: action.payload,
data: undefined,
};
case 'CLEAR_PREVIEW':
return { ...state, loading: false, error: undefined, data: undefined };
default:
return { ...state };
}
}
export default function DataGridPreviewCell<TData extends object>({
value: { fetchBlob, id, mimeType, alt, blob },
fallbackPreview = null,
}: DataGridPreviewCellProps<TData>) {
const { currentProject } = useCurrentWorkspaceAndProject();
const appClient = useAppClient();
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
const [showModal, setShowModal] = useState(false);
const [
{ loading: previewLoading, error: previewError, data: previewUrl },
dispatch,
] = useReducer(previewReducer, {
loading: false,
error: undefined,
data: undefined,
});
const isPreviewable = previewableFileTypes.some(
(type) => mimeType?.startsWith(type) || mimeType === type,
);
const isVideo = mimeType?.startsWith('video');
const isAudio = mimeType?.startsWith('audio');
const isImage = mimeType?.startsWith('image');
const isJson = mimeType === 'application/json';
async function handleOpenPreview() {
if (!mimeType) {
dispatch({
type: 'PREVIEW_ERROR',
payload: new Error('mimeType is not defined.'),
});
return;
}
if (isPreviewable) {
setShowModal(true);
dispatch({ type: 'PREVIEW_LOADING' });
}
const { presignedUrl } = await appClient.storage
.setAdminSecret(currentProject?.config?.hasura.adminSecret)
.getPresignedUrl({ fileId: id });
if (!presignedUrl) {
dispatch({
type: 'PREVIEW_ERROR',
payload: new Error('Presigned URL could not be fetched.'),
});
return;
}
if (!isPreviewable) {
window.open(presignedUrl.url, '_blank', 'noopener noreferrer');
return;
}
dispatch({ type: 'PREVIEW_FETCHED', payload: presignedUrl.url });
}
if (loading) {
return <ActivityIndicator delay={500} className="mx-auto" />;
}
if (error) {
return (
<Box
className="grid w-full grid-flow-col items-center justify-center gap-1 text-center"
sx={{ color: 'error.main' }}
>
<FilePreviewIcon error /> Error
</Box>
);
}
return (
<>
<Modal
wrapperClassName="items-center"
showModal={showModal}
close={() => setShowModal(false)}
afterLeave={() => dispatch({ type: 'CLEAR_PREVIEW' })}
className={clsx(
previewableImages.includes(mimeType) || isVideo || isAudio
? 'mx-12 flex h-screen items-center justify-center'
: 'mt-4 inline-block h-near-screen w-full px-12',
)}
>
<Box
className={clsx(
!isJson && 'bg-checker-pattern',
'relative mx-auto flex overflow-hidden rounded-md',
)}
sx={{
backgroundColor: isJson && 'background.default',
color: 'text.primary',
}}
>
{!previewLoading && (
<IconButton
aria-label="Close"
variant="borderless"
color="secondary"
className="absolute right-2 top-2 z-50 p-2"
sx={{
[`&:hover, &:active, &:focus`]: {
backgroundColor: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.black';
}
return theme.palette.mode === 'dark'
? 'grey.800'
: 'grey.200';
},
},
}}
onClick={() => setShowModal(false)}
>
<XIcon
className="h-5 w-5"
sx={{
color: (theme) => {
if (isAudio || isVideo || isJson) {
return 'common.white';
}
return theme.palette.mode === 'dark'
? 'grey.100'
: 'grey.700';
},
}}
/>
</IconButton>
)}
{previewLoading && !previewUrl && (
<ActivityIndicator
delay={500}
className="mx-auto"
label="Loading preview..."
/>
)}
{previewError && (
<Box
className="px-6 py-3.5 pr-12 text-start font-medium"
sx={{ color: 'error.main' }}
>
<p>Error: Preview can&apos;t be loaded.</p>
<p>{previewError.message}</p>
</Box>
)}
{previewUrl && isImage && (
<picture className="h-auto max-h-near-screen min-h-38 min-w-38">
<source srcSet={previewUrl} type={mimeType} />
<img
src={previewUrl}
alt={alt}
className="h-full w-full object-scale-down"
/>
</picture>
)}
{previewUrl && isVideo && (
<video
autoPlay
controls
className="h-auto max-h-near-screen w-full bg-black"
>
<track kind="captions" />
<source src={previewUrl} type={mimeType} />
Your browser does not support the video tag.
</video>
)}
{previewUrl && isAudio && (
<audio autoPlay controls className="h-28 bg-black">
<track kind="captions" />
<source src={previewUrl} type={mimeType} />
Your browser does not support the audio tag.
</audio>
)}
{!previewLoading &&
previewUrl &&
!previewableImages.includes(mimeType) &&
!isVideo &&
!isAudio && (
<iframe
src={previewUrl}
className="h-near-screen w-full"
title="File preview"
/>
)}
</Box>
</Modal>
<div className="flex h-full w-full justify-center">
{previewableImages.includes(mimeType) && objectUrl && (
<button
type="button"
aria-label={alt}
onClick={handleOpenPreview}
className="mx-auto h-full"
>
<picture className="h-full w-20">
<source srcSet={objectUrl} type={mimeType} />
<img
src={objectUrl}
alt={alt}
className="h-full w-full object-scale-down"
/>
</picture>
</button>
)}
{(!previewableImages.includes(mimeType) || !objectUrl) && (
<button
type="button"
onClick={handleOpenPreview}
aria-label={alt}
className="grid h-full w-full items-center justify-center self-center"
>
{isVideo && <VideoPreviewIcon className="h-5 w-5" />}
{isAudio && <AudioPreviewIcon className="h-5 w-5" />}
{mimeType === 'application/pdf' && (
<PDFPreviewIcon className="h-5 w-5" />
)}
{!isVideo &&
!isAudio &&
mimeType !== 'application/pdf' &&
fallbackPreview}
</button>
)}
</div>
</>
);
}

View File

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

View File

@@ -1,243 +0,0 @@
import type { CommonDataGridCellProps } from '@/components/dataGrid/DataGridCell';
import { useDataGridCell } from '@/components/dataGrid/DataGridCell';
import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { copy } from '@/utils/copy';
import type { ChangeEvent, KeyboardEvent, Ref } from 'react';
import { useEffect } from 'react';
export type DataGridTextCellProps<TData extends object> =
CommonDataGridCellProps<TData, string>;
export default function DataGridTextCell<TData extends object>({
onSave,
optimisticValue,
temporaryValue,
onTemporaryValueChange,
cell: {
column: { isCopiable, specificType },
},
}: DataGridTextCellProps<TData>) {
const isMultiline =
specificType === 'text' ||
specificType === 'bpchar' ||
specificType === 'varchar' ||
specificType === 'json' ||
specificType === 'jsonb';
const normalizedOptimisticValue =
optimisticValue !== null && typeof optimisticValue === 'object'
? optimisticValue
: (String(optimisticValue) || '').replace(/(\\n)+/gi, ' ');
const normalizedTemporaryValue =
temporaryValue !== null && typeof temporaryValue === 'object'
? JSON.stringify(temporaryValue)
: temporaryValue;
const { inputRef, focusCell, isEditing, cancelEditCell } = useDataGridCell<
HTMLInputElement | HTMLTextAreaElement
>();
useEffect(() => {
if (isEditing && isMultiline) {
const textArea = inputRef.current as HTMLTextAreaElement;
textArea.setSelectionRange(textArea.value.length, textArea.value.length);
}
}, [inputRef, isEditing, isMultiline]);
async function handleSave() {
if (onSave) {
await onSave((normalizedTemporaryValue || '').replace(/\n/gi, `\\n`));
}
}
async function handleInputKeyDown(event: KeyboardEvent<HTMLInputElement>) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
if (event.key === 'Tab') {
await handleSave();
}
if (event.key === 'Enter') {
await handleSave();
await focusCell();
cancelEditCell();
}
}
async function handleTextAreaKeyDown(
event: KeyboardEvent<HTMLTextAreaElement>,
) {
if (
event.key === 'ArrowLeft' ||
event.key === 'ArrowRight' ||
event.key === 'ArrowUp' ||
event.key === 'ArrowDown' ||
event.key === 'Backspace'
) {
event.stopPropagation();
}
// Saving content Enter / CTRL + Enter / CMD + Enter (macOS) - but not on
// Shift + Enter
if (
(!event.shiftKey && event.key === 'Enter') ||
(event.ctrlKey && event.key === 'Enter') ||
(event.metaKey && event.key === 'Enter')
) {
event.preventDefault();
event.stopPropagation();
await handleSave();
await focusCell();
cancelEditCell();
}
if (event.key === 'Tab') {
await handleSave();
}
}
function handleChange(
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) {
if (onTemporaryValueChange) {
onTemporaryValueChange(event.target.value);
}
}
if (isEditing && isMultiline) {
return (
<Input
multiline
ref={inputRef as Ref<HTMLInputElement>}
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange}
onKeyDown={handleTextAreaKeyDown}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full min-h-38"
rows={5}
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (isEditing) {
return (
<Input
ref={inputRef as Ref<HTMLInputElement>}
value={(normalizedTemporaryValue || '').replace(/\\n/gi, `\n`)}
onChange={handleChange}
onKeyDown={handleInputKeyDown}
fullWidth
className="absolute top-0 z-10 -mx-0.5 h-full place-content-stretch"
sx={{
[`&.${inputClasses.focused}`]: {
boxShadow: `inset 0 0 0 1.5px rgba(0, 82, 205, 1)`,
borderColor: 'transparent !important',
borderRadius: 0,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? `${theme.palette.secondary[100]} !important`
: `${theme.palette.common.white} !important`,
},
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent',
},
}}
slotProps={{
inputWrapper: { className: 'h-full' },
input: { className: 'h-full' },
inputRoot: {
className:
'resize-none outline-none focus:outline-none !text-xs focus:ring-0',
},
}}
/>
);
}
if (!optimisticValue) {
return (
<Text className="truncate !text-xs" color="secondary">
{optimisticValue === '' ? 'empty' : 'null'}
</Text>
);
}
if (isCopiable) {
return (
<div className="grid grid-flow-col items-center justify-start gap-1">
<Button
variant="borderless"
color="secondary"
onClick={(event) => {
event.stopPropagation();
const copiableValue =
typeof optimisticValue === 'object'
? JSON.stringify(optimisticValue)
: String(optimisticValue).replace(/\\n/gi, '\n');
copy(copiableValue, 'Value');
}}
className="-ml-px min-w-0 p-0"
aria-label="Copy value"
sx={{
color: (theme) =>
theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.disabled',
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
<Text className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue}
</Text>
</div>
);
}
return (
<Text className="truncate text-xs">
{typeof normalizedOptimisticValue === 'object'
? JSON.stringify(normalizedOptimisticValue)
: normalizedOptimisticValue}
</Text>
);
}

View File

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

View File

@@ -1,46 +0,0 @@
import { AISidebar } from '@/components/layout/AISidebar';
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Box } from '@/components/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
export interface AILayoutProps extends ProjectLayoutProps {
/**
* Props passed to the sidebar component.
*/
sidebarProps?: SettingsSidebarProps;
}
export default function AILayout({
children,
mainContainerProps: {
className: mainContainerClassName,
...mainContainerProps
} = {},
sidebarProps: { className: sidebarClassName, ...sidebarProps } = {},
...props
}: AILayoutProps) {
return (
<ProjectLayout
mainContainerProps={{
className: twMerge('flex h-full', mainContainerClassName),
...mainContainerProps,
}}
{...props}
>
<AISidebar
className={twMerge('w-full max-w-sidebar', sidebarClassName)}
{...sidebarProps}
/>
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
>
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
</Box>
</ProjectLayout>
);
}

View File

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

View File

@@ -1,143 +0,0 @@
import { NavLink } from '@/components/common/NavLink';
import { Backdrop } from '@/components/ui/v2/Backdrop';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface AISidebarProps extends Omit<BoxProps, 'children'> {}
interface AINavLinkProps extends ListItemButtonProps {
/**
* Link to navigate to.
*/
href: string;
/**
* Determines whether or not the link should be active if href matches the current route.
*
* @default true
*/
exact?: boolean;
}
function AINavLink({ exact = true, href, children, ...props }: AINavLinkProps) {
const router = useRouter();
const baseUrl = `/${router.query.workspaceSlug}/${router.query.appSlug}/ai`;
const finalUrl = href && href !== '/' ? `${baseUrl}${href}` : baseUrl;
const active = exact
? router.asPath === finalUrl
: router.asPath.startsWith(finalUrl);
return (
<ListItem.Root>
<ListItem.Button
dense
href={finalUrl}
component={NavLink}
selected={active}
{...props}
>
<ListItem.Text>{children}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
);
}
export default function AISidebar({ className, ...props }: AISidebarProps) {
const [expanded, setExpanded] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
function toggleExpanded() {
setExpanded(!expanded);
}
function handleSelect() {
setExpanded(false);
}
function closeSidebarWhenEscapeIsPressed(event: KeyboardEvent) {
if (event.key === 'Escape') {
setExpanded(false);
}
}
useEffect(() => {
if (typeof document !== 'undefined') {
document.addEventListener('keydown', closeSidebarWhenEscapeIsPressed);
}
return () =>
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
}, []);
if (!currentProject) {
return null;
}
return (
<>
<Backdrop
open={expanded}
className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
role="button"
tabIndex={-1}
onClick={() => setExpanded(false)}
aria-label="Close sidebar overlay"
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
setExpanded(false);
}}
/>
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
className,
)}
{...props}
>
<nav aria-label="Settings navigation">
<List className="grid gap-2">
<AINavLink
href="/auto-embeddings"
exact={false}
onClick={handleSelect}
>
Auto-Embeddings
</AINavLink>
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
Assistants
</AINavLink>
</List>
</nav>
</Box>
<IconButton
className="absolute bottom-4 left-4 z-[38] h-11 w-11 rounded-full md:hidden"
onClick={toggleExpanded}
aria-label="Toggle sidebar"
>
<Image
width={16}
height={16}
src="/assets/table.svg"
alt="A monochrome table"
/>
</IconButton>
</>
);
}

View File

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

View File

@@ -1,4 +1,3 @@
import { InviteNotification } from '@/components/common/InviteNotification';
import type { BaseLayoutProps } from '@/components/layout/BaseLayout'; import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
import { BaseLayout } from '@/components/layout/BaseLayout'; import { BaseLayout } from '@/components/layout/BaseLayout';
import { Container } from '@/components/layout/Container'; import { Container } from '@/components/layout/Container';
@@ -10,14 +9,14 @@ import { RetryableErrorBoundary } from '@/components/presentational/RetryableErr
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Link } from '@/components/ui/v2/Link'; import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform'; import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useAuthenticationStatus } from '@nhost/nextjs'; import { useAuthenticationStatus } from '@nhost/nextjs';
import { useMediaQuery } from '@/components/common/useMediaQuery'; import { useMediaQuery } from '@/components/common/useMediaQuery';
import PinnedMainNav from '@/components/layout/MainNav/PinnedMainNav'; import PinnedMainNav from '@/components/layout/MainNav/PinnedMainNav';
import { OrgStatus } from '@/features/orgs/components/OrgStatus'; import { OrgStatus } from '@/features/orgs/components/OrgStatus';
import { useIsHealthy } from '@/features/orgs/projects/common/hooks/useIsHealthy'; import { useIsHealthy } from '@/features/orgs/projects/common/hooks/useIsHealthy';
import { useNotFoundRedirect } from '@/features/projects/common/hooks/useNotFoundRedirect'; import { useNotFoundRedirect } from '@/features/orgs/projects/common/hooks/useNotFoundRedirect';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -98,7 +97,7 @@ export default function AuthenticatedLayout({
<HighlightedText className="font-mono">nhost up</HighlightedText>? <HighlightedText className="font-mono">nhost up</HighlightedText>?
Please refer to the{' '} Please refer to the{' '}
<Link <Link
href="https://docs.nhost.io/platform/cli" href="https://docs.nhost.io/platform/cli/local-development"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
underline="hover" underline="hover"
@@ -146,8 +145,6 @@ export default function AuthenticatedLayout({
{children} {children}
</div> </div>
</RetryableErrorBoundary> </RetryableErrorBoundary>
<InviteNotification />
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>

View File

@@ -1,78 +0,0 @@
import { NavLink } from '@/components/common/NavLink';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { twMerge } from 'tailwind-merge';
export interface BreadcrumbsProps extends BoxProps {}
export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
const isPlatform = useIsPlatform();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
if (!isPlatform) {
return (
<Box
className={twMerge(
'grid grid-flow-col items-center gap-3 text-sm font-medium',
className,
)}
{...props}
>
<Text color="disabled">/</Text>
<Text className="truncate text-[13px] sm:text-sm">local</Text>
<Text color="disabled">/</Text>
<NavLink
href="/local/local"
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
local
</NavLink>
</Box>
);
}
return (
<Box
className={twMerge(
'grid grid-flow-col items-center gap-3 text-sm font-medium',
className,
)}
{...props}
>
{currentWorkspace && (
<>
<Text color="disabled">/</Text>
<NavLink
href={`/${currentWorkspace.slug}`}
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
{currentWorkspace.name}
</NavLink>
</>
)}
{currentProject && (
<>
<Text color="disabled">/</Text>
<NavLink
href={`/${currentWorkspace.slug}/${currentProject.slug}`}
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
{currentProject.name}
</NavLink>
</>
)}
</Box>
);
}

View File

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

View File

@@ -1,90 +0,0 @@
import type { IconLinkProps } from '@/components/common/IconLink';
import { IconLink } from '@/components/common/IconLink';
import { Nav } from '@/components/presentational/Nav';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { useRouter } from 'next/router';
import { twMerge } from 'tailwind-merge';
export interface DesktopNavProps extends Omit<BoxProps, 'children'> {}
interface DesktopNavLinkProps extends IconLinkProps {
/**
* Determines whether or not the link should be active if it's href exactly
* matches the current route.
*
* @default true
*/
exact?: boolean;
/**
* Path of the link.
*/
path?: string;
}
function DesktopNavLink({
exact = true,
href,
path,
...props
}: DesktopNavLinkProps) {
const router = useRouter();
const baseUrl = `/${router.query.workspaceSlug}/${router.query.appSlug}`;
const finalUrl = href && href !== '/' ? `${baseUrl}${href}` : baseUrl;
const finalRelativePath =
path && path !== '/' ? `${baseUrl}${path}` : baseUrl;
const active = exact
? router.asPath === finalUrl
: router.asPath.startsWith(finalRelativePath);
return (
<li>
<IconLink {...props} href={finalUrl} active={props.active || active} />
</li>
);
}
export default function DesktopNav({ className, ...props }: DesktopNavProps) {
const { allRoutes } = useProjectRoutes();
return (
<Box
className={twMerge(
'w-20 content-start overflow-hidden overflow-y-auto border-r-1 px-1 pb-10',
className,
)}
{...props}
>
<Nav
aria-label="Main navigation"
className="w-full"
flow="row"
listProps={{ className: 'gap-2 justify-center py-2' }}
>
{allRoutes.map(
({
relativePath,
relativeMainPath,
label,
icon,
exact,
disabled,
}) => (
<DesktopNavLink
href={relativePath}
path={relativeMainPath || relativePath}
exact={exact}
icon={icon}
key={relativePath}
disabled={disabled}
>
{label}
</DesktopNavLink>
),
)}
</Nav>
</Box>
);
}

View File

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

View File

@@ -1,109 +0,0 @@
import { Button } from '@/components/ui/v3/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/v3/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/v3/popover';
import { cn } from '@/lib/utils';
import { Check, ChevronsUpDown } from 'lucide-react';
import { useState, type ReactNode } from 'react';
type BreadCrumbComboBoxItem<T> = {
label: string | ReactNode;
value: string | T;
};
interface BreadCrumbComboBoxProps<T> {
selectedValue?: T;
options: BreadCrumbComboBoxItem<T>[];
renderItem?: (item: T) => ReactNode;
onChange?: (item: BreadCrumbComboBoxItem<T>) => void;
filter?: (value: string, search: string) => number;
}
export default function BreadCrumbComboBox<T>({
selectedValue,
options,
renderItem,
onChange,
filter,
}: BreadCrumbComboBoxProps<T>) {
const [open, setOpen] = useState(false);
const [selectedItem, setSelectedItem] =
useState<BreadCrumbComboBoxItem<T> | null>(
options.find((option) => option.value === selectedValue) || null,
);
const renderSelectedItem = (item: BreadCrumbComboBoxItem<T>) => {
if (typeof item.value === 'string') {
return typeof item.label === 'string' ? (
<span className="text-foreground">{item.label}</span>
) : (
item.label
);
}
return renderItem ? renderItem(item.value) : null;
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="sm"
className="justify-start text-foreground"
>
<div className="flex flex-row items-center justify-center gap-1">
{selectedItem && renderSelectedItem(selectedItem)}
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" side="bottom" align="start">
<Command filter={filter}>
<CommandInput placeholder="Search..." autoFocus />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option, index) => (
<CommandItem
key={`${
typeof option.value === 'string' ? option.value : index
}`}
value={
typeof option.value === 'string' ? option.value : `${index}`
}
onSelect={() => {
setSelectedItem(option);
setOpen(false);
onChange?.(option);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedItem?.value === option.value
? 'opacity-100'
: 'opacity-0',
)}
/>
{typeof option.value === 'string'
? option.label
: renderItem && renderItem(option.value)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -17,8 +17,7 @@ import ProjectSettingsPagesComboBox from './ProjectSettingsPagesComboBox';
export default function BreadcrumbNav() { export default function BreadcrumbNav() {
const { query, asPath, route } = useRouter(); const { query, asPath, route } = useRouter();
// Extract orgSlug and appSubdomain from router.query const { appSubdomain } = query;
const { appSubdomain, workspaceSlug } = query;
// Extract path segments from the URL // Extract path segments from the URL
const pathSegments = useMemo(() => asPath.split('/'), [asPath]); const pathSegments = useMemo(() => asPath.split('/'), [asPath]);
@@ -27,8 +26,7 @@ export default function BreadcrumbNav() {
const projectPage = pathSegments[3] || null; const projectPage = pathSegments[3] || null;
const isSettingsPage = pathSegments[5] === 'settings'; const isSettingsPage = pathSegments[5] === 'settings';
const showBreadcrumbs = const showBreadcrumbs = !['/', '/orgs/verify'].includes(route);
!workspaceSlug && !['/', '/orgs/verify'].includes(route);
return ( return (
<Breadcrumb className="mt-2 flex w-full flex-row flex-nowrap overflow-x-auto lg:mt-0 lg:overflow-visible"> <Breadcrumb className="mt-2 flex w-full flex-row flex-nowrap overflow-x-auto lg:mt-0 lg:overflow-visible">

View File

@@ -7,14 +7,11 @@ import { Logo } from '@/components/presentational/Logo';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon'; import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
import { Button } from '@/components/ui/v3/button'; import { Button } from '@/components/ui/v3/button';
import { DevAssistant as WorkspaceProjectDevAssistant } from '@/features/ai/DevAssistant';
import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray'; import { AnnouncementsTray } from '@/features/orgs/components/members/components/AnnouncementsTray';
import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray'; import { NotificationsTray } from '@/features/orgs/components/members/components/NotificationsTray';
import { DevAssistant } from '@/features/orgs/projects/ai/DevAssistant'; import { DevAssistant } from '@/features/orgs/projects/ai/DevAssistant';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs'; import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useProject } from '@/features/orgs/projects/hooks/useProject'; import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react'; import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@@ -30,12 +27,10 @@ export default function Header({ className, ...props }: HeaderProps) {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { openDrawer } = useDialog(); const { openDrawer } = useDialog();
const { project } = useProject(); const { project } = useProject();
const { currentProject: workspaceProject } = useCurrentWorkspaceAndProject();
const { currentOrg: org } = useOrgs();
const openDevAssistant = () => { const openDevAssistant = () => {
// The dev assistant can be only answer questions related to a particular project // The dev assistant can be only answer questions related to a particular project
if (!project && !workspaceProject) { if (!project) {
toast.error('You need to be inside a project to open the Assistant', { toast.error('You need to be inside a project to open the Assistant', {
style: getToastStyleProps().style, style: getToastStyleProps().style,
...getToastStyleProps().error, ...getToastStyleProps().error,
@@ -44,17 +39,10 @@ export default function Header({ className, ...props }: HeaderProps) {
return; return;
} }
if (org && project) { openDrawer({
openDrawer({ title: <GraphiteIcon />,
title: <GraphiteIcon />, component: <DevAssistant />,
component: <DevAssistant />, });
});
} else {
openDrawer({
title: <GraphiteIcon />,
component: <WorkspaceProjectDevAssistant />,
});
}
}; };
return ( return (

View File

@@ -7,7 +7,6 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator,
} from '@/components/ui/v3/command'; } from '@/components/ui/v3/command';
import { import {
Popover, Popover,
@@ -16,7 +15,6 @@ import {
} from '@/components/ui/v3/popover'; } from '@/components/ui/v3/popover';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform'; import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs'; import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { useWorkspaces } from '@/features/orgs/projects/hooks/useWorkspaces';
import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage'; import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Check, ChevronsUpDown } from 'lucide-react'; import { Check, ChevronsUpDown } from 'lucide-react';
@@ -27,53 +25,39 @@ type Option = {
value: string; value: string;
label: string; label: string;
plan: string; plan: string;
type: 'organization' | 'workspace';
}; };
export default function OrgsComboBox() { export default function OrgsComboBox() {
const { orgs } = useOrgs(); const { orgs } = useOrgs();
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { workspaces } = useWorkspaces();
const [, setLastSlug] = useSSRLocalStorage('slug', null); const [, setLastSlug] = useSSRLocalStorage('slug', null);
const { const {
query: { orgSlug, workspaceSlug }, query: { orgSlug },
push, push,
} = useRouter(); } = useRouter();
const selectedOrgFromUrl = const selectedOrgFromUrl =
Boolean(orgSlug) && orgs.find((item) => item.slug === orgSlug); Boolean(orgSlug) && orgs.find((item) => item.slug === orgSlug);
const selectedWorkspaceFromUrl =
Boolean(workspaceSlug) &&
workspaces.find((item) => item.slug === workspaceSlug);
const [selectedItem, setSelectedItem] = useState<Option | null>(null); const [selectedItem, setSelectedItem] = useState<Option | null>(null);
useEffect(() => { useEffect(() => {
const selectedItemFromUrl = selectedOrgFromUrl || selectedWorkspaceFromUrl; const selectedItemFromUrl = selectedOrgFromUrl;
if (selectedItemFromUrl) { if (selectedItemFromUrl) {
setSelectedItem({ setSelectedItem({
label: selectedItemFromUrl.name, label: selectedItemFromUrl.name,
value: selectedItemFromUrl.slug, value: selectedItemFromUrl.slug,
plan: selectedOrgFromUrl ? selectedOrgFromUrl.plan.name : 'Legacy', plan: selectedOrgFromUrl ? selectedOrgFromUrl.plan.name : 'Legacy',
type: selectedOrgFromUrl ? 'organization' : 'workspace',
}); });
} }
}, [selectedOrgFromUrl, selectedWorkspaceFromUrl]); }, [selectedOrgFromUrl]);
const orgsOptions: Option[] = orgs.map((org) => ({ const orgsOptions: Option[] = orgs.map((org) => ({
label: org.name, label: org.name,
value: org.slug, value: org.slug,
plan: org.plan.name, plan: org.plan.name,
type: 'organization',
}));
const workspacesOptions: Option[] = workspaces.map((workspace) => ({
label: workspace.name,
value: workspace.slug,
plan: 'Legacy',
type: 'workspace',
})); }));
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -113,7 +97,7 @@ export default function OrgsComboBox() {
{renderBadge(selectedItem.plan)} {renderBadge(selectedItem.plan)}
</div> </div>
) : ( ) : (
'Select organization / workspace' 'Select organization'
)} )}
<ChevronsUpDown className="h-5 w-5 text-muted-foreground" /> <ChevronsUpDown className="h-5 w-5 text-muted-foreground" />
</Button> </Button>
@@ -137,11 +121,7 @@ export default function OrgsComboBox() {
// persist last slug in local storage // persist last slug in local storage
setLastSlug(option.value); setLastSlug(option.value);
if (option.type === 'organization') { push(`/orgs/${option.value}/projects`);
push(`/orgs/${option.value}/projects`);
} else {
push(`/${option.value}`);
}
}} }}
> >
<Check <Check
@@ -157,47 +137,6 @@ export default function OrgsComboBox() {
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
{workspaces.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Workspaces">
{workspacesOptions.map((option) => (
<CommandItem
keywords={[option.label]}
key={option.value}
value={option.value}
className="flex items-center text-foreground dark:hover:bg-muted"
onSelect={() => {
setSelectedItem(option);
setOpen(false);
// persist last slug in local storage
setLastSlug(option.value);
if (option.type === 'organization') {
push(`/orgs/${option.value}/projects`);
} else {
push(`/${option.value}`);
}
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedItem?.value === option.value
? 'opacity-100'
: 'opacity-0',
)}
/>
<span className="w-full truncate">{option.label}</span>
{renderBadge(option.plan)}
</CommandItem>
))}
</CommandGroup>
</>
)}
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>

View File

@@ -1,5 +1,4 @@
import { Button } from '@/components/ui/v3/button'; import { Button } from '@/components/ui/v3/button';
import { Separator } from '@/components/ui/v3/separator';
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@@ -8,13 +7,11 @@ import {
SheetTitle, SheetTitle,
} from '@/components/ui/v3/sheet'; } from '@/components/ui/v3/sheet';
import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog'; import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog';
import { useWorkspaces } from '@/features/orgs/projects/hooks/useWorkspaces';
import { Menu, Pin, PinOff, X } from 'lucide-react'; import { Menu, Pin, PinOff, X } from 'lucide-react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import NavTree from './NavTree'; import NavTree from './NavTree';
import { useTreeNavState } from './TreeNavStateContext'; import { useTreeNavState } from './TreeNavStateContext';
import WorkspacesNavTree from './WorkspacesNavTree';
interface MainNavProps { interface MainNavProps {
container: HTMLElement; container: HTMLElement;
@@ -22,7 +19,6 @@ interface MainNavProps {
export default function MainNav({ container }: MainNavProps) { export default function MainNav({ container }: MainNavProps) {
const { asPath } = useRouter(); const { asPath } = useRouter();
const { workspaces } = useWorkspaces();
const scrollContainerRef = useRef<HTMLDivElement | null>(null); const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const { open, setOpen, mainNavPinned, setMainNavPinned } = useTreeNavState(); const { open, setOpen, mainNavPinned, setMainNavPinned } = useTreeNavState();
@@ -95,14 +91,6 @@ export default function MainNav({ container }: MainNavProps) {
<NavTree /> <NavTree />
<CreateOrgDialog /> <CreateOrgDialog />
</div> </div>
{workspaces.length > 0 && (
<>
<Separator className="mx-auto my-2" />
<div className="px-2">
<WorkspacesNavTree />
</div>
</>
)}
</div> </div>
</SheetContent> </SheetContent>
</Sheet> </Sheet>

View File

@@ -1,22 +1,18 @@
import NavTree from '@/components/layout/MainNav/NavTree'; import NavTree from '@/components/layout/MainNav/NavTree';
import { Button } from '@/components/ui/v3/button'; import { Button } from '@/components/ui/v3/button';
import { Separator } from '@/components/ui/v3/separator';
import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog'; import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog';
import { useWorkspaces } from '@/features/orgs/projects/hooks/useWorkspaces';
import { Pin, PinOff } from 'lucide-react'; import { Pin, PinOff } from 'lucide-react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useTreeNavState } from './TreeNavStateContext'; import { useTreeNavState } from './TreeNavStateContext';
import WorkspacesNavTree from './WorkspacesNavTree';
export default function PinnedMainNav() { export default function PinnedMainNav() {
const { const {
asPath, asPath,
query: { workspaceSlug, orgSlug }, query: { orgSlug },
} = useRouter(); } = useRouter();
const scrollContainerRef = useRef(); const scrollContainerRef = useRef();
const { workspaces } = useWorkspaces();
const { mainNavPinned, setMainNavPinned } = useTreeNavState(); const { mainNavPinned, setMainNavPinned } = useTreeNavState();
useEffect(() => { useEffect(() => {
@@ -48,7 +44,7 @@ export default function PinnedMainNav() {
}; };
}, [asPath]); }, [asPath]);
if (!orgSlug && !workspaceSlug) { if (!orgSlug) {
return null; return null;
} }
@@ -75,14 +71,6 @@ export default function PinnedMainNav() {
<NavTree /> <NavTree />
<CreateOrgDialog /> <CreateOrgDialog />
</div> </div>
{workspaces.length > 0 && (
<>
<Separator className="mx-auto my-2" />
<div className="px-2">
<WorkspacesNavTree />
</div>
</>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,4 @@
import { useNavTreeStateFromURL } from '@/features/orgs/projects/hooks/useNavTreeStateFromURL'; import { useNavTreeStateFromURL } from '@/features/orgs/projects/hooks/useNavTreeStateFromURL';
import { useWorkspacesNavTreeStateFromURL } from '@/features/orgs/projects/hooks/useWorkspacesNavTreeStateFromURL';
import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage'; import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage';
import { import {
createContext, createContext,
@@ -21,10 +20,6 @@ interface TreeNavStateContextType {
setOrgsTreeViewState: Dispatch< setOrgsTreeViewState: Dispatch<
SetStateAction<IndividualTreeViewState<never>> SetStateAction<IndividualTreeViewState<never>>
>; >;
workspacesTreeViewState: IndividualTreeViewState<never>;
setWorkspacesTreeViewState: Dispatch<
SetStateAction<IndividualTreeViewState<never>>
>;
setMainNavPinned: (value: boolean) => void; setMainNavPinned: (value: boolean) => void;
} }
@@ -71,10 +66,6 @@ function TreeNavStateProvider({ children }: TreeNavProviderProps) {
); );
const orgsTreeViewState = useSyncedTreeViewState(useNavTreeStateFromURL); const orgsTreeViewState = useSyncedTreeViewState(useNavTreeStateFromURL);
const workspacesTreeViewState = useSyncedTreeViewState(
useWorkspacesNavTreeStateFromURL,
);
const value = useMemo( const value = useMemo(
() => ({ () => ({
open, open,
@@ -83,8 +74,6 @@ function TreeNavStateProvider({ children }: TreeNavProviderProps) {
setMainNavPinned, setMainNavPinned,
orgsTreeViewState: orgsTreeViewState.state, orgsTreeViewState: orgsTreeViewState.state,
setOrgsTreeViewState: orgsTreeViewState.setState, setOrgsTreeViewState: orgsTreeViewState.setState,
workspacesTreeViewState: workspacesTreeViewState.state,
setWorkspacesTreeViewState: workspacesTreeViewState.setState,
}), }),
[ [
open, open,
@@ -93,8 +82,6 @@ function TreeNavStateProvider({ children }: TreeNavProviderProps) {
setMainNavPinned, setMainNavPinned,
orgsTreeViewState.state, orgsTreeViewState.state,
orgsTreeViewState.setState, orgsTreeViewState.setState,
workspacesTreeViewState.state,
workspacesTreeViewState.setState,
], ],
); );

View File

@@ -1,495 +0,0 @@
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { CloudIcon } from '@/components/ui/v2/icons/CloudIcon';
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 { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Link } from '@/components/ui/v2/Link';
import { Button } from '@/components/ui/v3/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/v3/hover-card';
import { useWorkspaces } from '@/features/orgs/projects/hooks/useWorkspaces';
import { type Workspace } from '@/features/orgs/projects/hooks/useWorkspaces/useWorkspaces';
import { useSSRLocalStorage } from '@/hooks/useSSRLocalStorage';
import { cn } from '@/lib/utils';
import { Box, ChevronDown, ChevronRight } from 'lucide-react';
import NextLink from 'next/link';
import { type ReactElement } from 'react';
import {
ControlledTreeEnvironment,
Tree,
type TreeItem,
type TreeItemIndex,
} from 'react-complex-tree';
import { useTreeNavState } from './TreeNavStateContext';
const projectPages = [
{
name: 'Overview',
icon: <HomeIcon className="h-4 w-4" />,
route: '',
slug: 'overview',
},
{
name: 'Database',
icon: <DatabaseIcon className="h-4 w-4" />,
route: 'database/browser/default',
slug: 'database',
},
{
name: 'GraphQL',
icon: <GraphQLIcon className="h-4 w-4" />,
route: 'graphql',
slug: 'graphql',
},
{
name: 'Hasura',
icon: <HasuraIcon className="h-4 w-4" />,
route: 'hasura',
slug: 'hasura',
},
{
name: 'Auth',
icon: <UserIcon className="h-4 w-4" />,
route: 'users',
slug: 'users',
},
{
name: 'Storage',
icon: <StorageIcon className="h-4 w-4" />,
route: 'storage',
slug: 'storage',
},
{
name: 'Run',
icon: <ServicesIcon className="h-4 w-4" />,
route: 'services',
slug: 'services',
},
{
name: 'AI',
icon: <AIIcon className="h-4 w-4" />,
route: 'ai/auto-embeddings',
slug: 'ai',
},
{
name: 'Deployments',
icon: <RocketIcon className="h-4 w-4" />,
route: 'deployments',
slug: 'deployments',
},
{
name: 'Backups',
icon: <CloudIcon className="h-4 w-4" />,
route: 'backups',
slug: 'backups',
},
{
name: 'Logs',
icon: <FileTextIcon className="h-4 w-4" />,
route: 'logs',
slug: 'logs',
},
{
name: 'Metrics',
icon: <GaugeIcon className="h-4 w-4" />,
route: 'metrics',
slug: 'metrics',
},
{
name: 'Settings',
route: 'settings/general',
slug: 'settings',
},
];
const projectSettingsPages = [
{ name: 'General', slug: 'general', route: 'general' },
{
name: 'Compute Resources',
slug: 'resources',
route: 'resources',
},
{ name: 'Database', slug: 'database', route: 'database' },
{ name: 'Hasura', slug: 'hasura', route: 'hasura' },
{
name: 'Authentication',
slug: 'authentication',
route: 'authentication',
},
{
name: 'Sign-In methods',
slug: 'sign-in-methods',
route: 'sign-in-methods',
},
{ name: 'Storage', slug: 'storage', route: 'storage' },
{
name: 'Roles and Permissions',
slug: 'roles-and-permissions',
route: 'roles-and-permissions',
},
{ name: 'SMTP', slug: 'smtp', route: 'smtp' },
{ name: 'Git', slug: 'git', route: 'git' },
{
name: 'Environment Variables',
slug: 'environment-variables',
route: 'environment-variables',
},
{ name: 'Secrets', slug: 'secrets', route: 'secrets' },
{
name: 'Custom Domains',
slug: 'custom-domains',
route: 'custom-domains',
},
{
name: 'Rate Limiting',
slug: 'rate-limiting',
route: 'rate-limiting',
},
{ name: 'AI', slug: 'ai', route: 'ai' },
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
];
const createWorkspace = (workspace: Workspace) => {
const result = {};
result[workspace.slug] = {
index: workspace.slug,
canMove: false,
isFolder: true,
children: [`${workspace.slug}-overview`, `${workspace.slug}-projects`],
data: {
name: workspace.name,
slug: workspace.slug,
type: 'workspace',
targetUrl: `/${workspace.slug}`,
},
canRename: false,
};
result[`${workspace.slug}-overview`] = {
index: `${workspace.slug}-overview`,
canMove: false,
isFolder: false,
children: null,
data: {
name: 'Overview',
targetUrl: `/${workspace.slug}`,
},
canRename: false,
};
result[`${workspace.slug}-projects`] = {
index: `${workspace.slug}-projects`,
canMove: false,
isFolder: true,
children: workspace.projects.map((app) => `${workspace.slug}-${app.slug}`),
data: {
name: 'Projects',
},
canRename: false,
};
workspace.projects.forEach((app) => {
result[`${workspace.slug}-${app.slug}`] = {
index: `${workspace.slug}-${app.slug}`,
isFolder: true,
canMove: false,
canRename: false,
data: {
name: app.name,
slug: app.slug,
icon: <Box className="h-4 w-4" />,
targetUrl: `/${workspace.slug}/${app.slug}`,
},
children: projectPages.map(
(page) => `${workspace.slug}-${app.slug}-${page.slug}`,
),
};
});
workspace.projects.forEach((_app) => {
projectPages.forEach((_page) => {
result[`${workspace.slug}-${_app.slug}-${_page.slug}`] = {
index: `${workspace.slug}-${_app.slug}-${_page.slug}`,
canMove: false,
isFolder: _page.name === 'Settings',
children:
_page.name === 'Settings'
? projectSettingsPages.map(
(p) => `${workspace.slug}-${_app.slug}-settings-${p.slug}`,
)
: undefined,
data: {
name: _page.name,
icon: _page.icon,
isProjectPage: true,
targetUrl: `/${workspace.slug}/${_app.slug}/${_page.route}`,
},
canRename: false,
};
});
// add the settings pages
projectSettingsPages.forEach((p) => {
result[`${workspace.slug}-${_app.slug}-settings-${p.slug}`] = {
index: `${workspace.slug}-${_app.slug}-settings-${p.slug}`,
canMove: false,
isFolder: false,
children: undefined,
data: {
name: p.name,
targetUrl: `/${workspace.slug}/${_app.slug}/settings/${p.route}`,
},
canRename: false,
};
});
});
return result;
};
type NavItem = {
name: string;
slug?: string;
type?: string;
icon?: ReactElement;
targetUrl?: string;
};
const buildNavTreeData = (
workspaces: Workspace[],
): { items: Record<TreeItemIndex, TreeItem<NavItem>> } => {
const navTree = {
items: {
root: {
index: 'root',
canMove: false,
isFolder: true,
children: ['workspaces'],
data: { name: 'root' },
canRename: false,
},
workspaces: {
index: 'workspaces',
canMove: false,
isFolder: true,
children: workspaces.map((workspace) => workspace.slug),
data: { name: 'Workspaces', type: 'workspaces-root' },
canRename: false,
},
...workspaces.reduce(
(acc, workspace) => ({ ...acc, ...createWorkspace(workspace) }),
{},
),
},
};
return navTree;
};
export default function WorkspacesNavTree() {
const { workspaces } = useWorkspaces();
const navTree = buildNavTreeData(workspaces);
const [, setLastSlug] = useSSRLocalStorage('slug', null);
const { workspacesTreeViewState, setWorkspacesTreeViewState, setOpen } =
useTreeNavState();
const renderItem = ({ arrow, context, item, children }) => {
const navItemContent = () => (
<>
{item.data.icon && (
<span
className={cn(
'flex items-start',
context.isFocused ? 'text-primary' : '',
)}
>
{item.data.icon}
</span>
)}
<span
className={cn(
item?.index === 'workspaces' && 'font-bold',
context.isFocused ? 'font-bold text-primary' : '',
'max-w-40 truncate',
)}
>
{item.data.name}
</span>
{item.data.type === 'workspaces-root' && (
<HoverCard openDelay={0}>
<HoverCardTrigger asChild>
<div
className={cn(
'h-5 rounded-full bg-muted bg-orange-200 px-[6px] text-[10px] dark:bg-orange-500',
)}
>
Legacy
</div>
</HoverCardTrigger>
<HoverCardContent className="w-64" side="top">
<div className="whitespace-normal">
<span>For more information read the </span>
<Link
href="https://nhost.io/blog/organization-billing"
target="_blank"
rel="noopener noreferrer"
className="font-medium"
>
announcement
<ArrowSquareOutIcon className="mb-1 ml-1 h-4 w-4" />
</Link>
</div>
</HoverCardContent>
</HoverCard>
)}
</>
);
return (
<li
{...context.itemContainerWithChildrenProps}
className="flex flex-col gap-1"
>
<div className="flex flex-row items-center">
{arrow}
<Button
asChild
onClick={() => {
if (item.data.type !== 'workspace') {
context.focusItem();
} else {
// persist last slug if the nav item is a workspace
setLastSlug(item.data.slug);
}
}}
className={cn(
'flex h-8 w-full flex-row justify-start gap-1 bg-background px-1 text-foreground hover:bg-accent dark:hover:bg-muted',
context.isFocused &&
'bg-[#ebf3ff] hover:bg-[#ebf3ff] dark:bg-muted',
item.data.disabled && 'pointer-events-none opacity-50',
)}
>
{item.data.targetUrl ? (
<NextLink
href={item.data.targetUrl || '/'}
onClick={() => setOpen(false)}
>
{navItemContent()}
</NextLink>
) : (
<div className="cursor-pointer">{navItemContent()}</div>
)}
</Button>
</div>
<div>{children}</div>
</li>
);
};
return (
<ControlledTreeEnvironment
items={navTree.items}
getItemTitle={(item) => item.data.name}
viewState={{
'workspaces-nav-tree': workspacesTreeViewState,
}}
renderItemTitle={({ title }) => <span>{title}</span>}
renderItemArrow={({ item, context }) => {
if (!item.isFolder) {
return null;
}
return (
<Button
type="button"
variant="ghost"
onClick={() => context.toggleExpandedState()}
className="h-8 px-1"
>
{context.isExpanded ? (
<ChevronDown className="h-4 w-4 font-bold" strokeWidth={3} />
) : (
<ChevronRight className="h-4 w-4" strokeWidth={3} />
)}
</Button>
);
}}
renderItem={renderItem}
renderTreeContainer={({ children, containerProps }) => (
<div {...containerProps} className="w-full">
{children}
</div>
)}
renderItemsContainer={({ children, containerProps, depth }) => {
if (depth === 0) {
return (
<ul {...containerProps} className="w-full">
{children}
</ul>
);
}
return (
<div className="flex w-full flex-row">
<div className="flex justify-center px-[12px] pb-3">
<div className="h-full w-0 border-r border-dashed" />
</div>
<ul {...containerProps} className="w-full">
{children}
</ul>
</div>
);
}}
canSearch={false}
onExpandItem={(item) => {
setWorkspacesTreeViewState(
({ expandedItems: prevExpandedItems, ...rest }) => ({
...rest,
// Add item index to expandedItems only if it's not already present
expandedItems: prevExpandedItems.includes(item.index)
? prevExpandedItems
: [...prevExpandedItems, item.index],
}),
);
}}
onCollapseItem={(item) => {
setWorkspacesTreeViewState(
({ expandedItems: prevExpandedItems, ...rest }) => ({
...rest,
// Remove the item index from expandedItems
expandedItems: prevExpandedItems.filter(
(index) => index !== item.index,
),
}),
);
}}
onFocusItem={(item) => {
setWorkspacesTreeViewState((prevViewState) => ({
...prevViewState,
// Set the focused item
focusedItem: item.index,
}));
}}
>
<Tree
rootItem="root"
treeId="workspaces-nav-tree"
treeLabel="Workspaces Navigation Tree"
/>
</ControlledTreeEnvironment>
);
}

View File

@@ -1,88 +1,26 @@
import { ContactUs } from '@/components/common/ContactUs';
import { NavLink } from '@/components/common/NavLink'; import { NavLink } from '@/components/common/NavLink';
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher'; import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
import { Nav } from '@/components/presentational/Nav';
import type { ButtonProps } from '@/components/ui/v2/Button'; import type { ButtonProps } from '@/components/ui/v2/Button';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider'; import { Divider } from '@/components/ui/v2/Divider';
import { Drawer } from '@/components/ui/v2/Drawer'; import { Drawer } from '@/components/ui/v2/Drawer';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { MenuIcon } from '@/components/ui/v2/icons/MenuIcon'; import { MenuIcon } from '@/components/ui/v2/icons/MenuIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon'; import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { List } from '@/components/ui/v2/List'; import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem'; import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform'; import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useNavigationVisible } from '@/features/projects/common/hooks/useNavigationVisible';
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useSignOut } from '@nhost/nextjs'; import { useSignOut } from '@nhost/nextjs';
import getConfig from 'next/config'; import getConfig from 'next/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { ReactNode } from 'react'; import { useState } from 'react';
import { cloneElement, Fragment, isValidElement, useState } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface MobileNavProps extends ButtonProps {} 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) { export default function MobileNav({ className, ...props }: MobileNavProps) {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { allRoutes } = useProjectRoutes();
const shouldDisplayNav = useNavigationVisible();
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const { signOut } = useSignOut(); const { signOut } = useSignOut();
const apolloClient = useApolloClient(); 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', className: 'w-full px-4 pt-18 pb-12 grid grid-flow-row gap-6',
}} }}
> >
{shouldDisplayNav && ( <section className="mt-2 grid grid-flow-row gap-3">
<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',
)}
>
<Text variant="h2" className="text-xl font-semibold"> <Text variant="h2" className="text-xl font-semibold">
Resources Resources
</Text> </Text>
<List className="grid grid-flow-row gap-2"> <List className="grid grid-flow-row gap-2">
{isPlatform && ( {isPlatform && (
<Dropdown.Root> <ListItem.Root>
<Dropdown.Trigger <ListItem.Button
className="justify-initial w-full" component={NavLink}
hideChevron href="/support"
asChild target="_blank"
rel="noopener noreferrer"
> >
<ListItem.Root> <ListItem.Text>Contact us</ListItem.Text>
<ListItem.Button </ListItem.Button>
component="span" </ListItem.Root>
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>
)} )}
<Divider component="li" /> <Divider component="li" />

View File

@@ -1,123 +0,0 @@
import type { AuthenticatedLayoutProps } from '@/components/layout/AuthenticatedLayout';
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { DesktopNav } from '@/components/layout/DesktopNav';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useNavigationVisible } from '@/features/projects/common/hooks/useNavigationVisible';
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { twMerge } from 'tailwind-merge';
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
/**
* Props passed to the internal `<main />` element.
*/
mainContainerProps?: BoxProps;
}
function ProjectLayoutContent({
children,
mainContainerProps: {
className: mainContainerClassName,
...mainContainerProps
} = {},
}: ProjectLayoutProps) {
const { currentProject, loading, error } = useCurrentWorkspaceAndProject();
const router = useRouter();
const shouldDisplayNav = useNavigationVisible();
const isPlatform = useIsPlatform();
const { nhostRoutes } = useProjectRoutes();
const pathWithoutWorkspaceAndProject = router.asPath.replace(
/^\/[\w\-_[\]]+\/[\w\-_[\]]+/i,
'',
);
const isRestrictedPath =
!isPlatform &&
nhostRoutes.some((route) =>
pathWithoutWorkspaceAndProject.startsWith(
route.relativeMainPath || route.relativePath,
),
);
// useNotFoundRedirect();
// useEffect(() => {
// if (isPlatform || !router.isReady) {
// return;
// }
// TODO // Double check what restricted path means here
// if (isRestrictedPath) {
// router.push('/local/local');
// }
// }, [isPlatform, isRestrictedPath, router]);
if (isRestrictedPath || loading) {
return <LoadingScreen />;
}
if (error) {
throw error;
}
if (!isPlatform) {
return (
<>
<DesktopNav className="top-0 hidden w-20 shrink-0 flex-col items-start sm:flex" />
<Box
component="main"
className={twMerge(
'relative flex-auto overflow-y-auto',
mainContainerClassName,
)}
{...mainContainerProps}
>
{children}
<NextSeo title="Local App" />
</Box>
</>
);
}
return (
<>
{shouldDisplayNav && (
<DesktopNav className="top-0 hidden w-20 shrink-0 flex-col items-start sm:flex" />
)}
<Box
component="main"
className={twMerge(
'relative flex-auto overflow-y-auto',
mainContainerClassName,
)}
{...mainContainerProps}
>
{children}
<NextSeo title={currentProject?.name} />
</Box>
</>
);
}
export default function OrgProjectLayout({
children,
mainContainerProps,
...props
}: ProjectLayoutProps) {
return (
<AuthenticatedLayout {...props}>
<ProjectLayoutContent mainContainerProps={mainContainerProps}>
{children}
</ProjectLayoutContent>
</AuthenticatedLayout>
);
}

View File

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

View File

@@ -1,114 +0,0 @@
import type { AuthenticatedLayoutProps } from '@/components/layout/AuthenticatedLayout';
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
/**
* Props passed to the internal `<main />` element.
*/
mainContainerProps?: BoxProps;
}
function ProjectLayoutContent({
children,
mainContainerProps: {
className: mainContainerClassName,
...mainContainerProps
} = {},
}: ProjectLayoutProps) {
const { project, loading, error } = useProject();
const router = useRouter();
const isPlatform = useIsPlatform();
const { nhostRoutes } = useProjectRoutes();
const pathWithoutWorkspaceAndProject = router.asPath.replace(
/^\/[\w\-_[\]]+\/[\w\-_[\]]+/i,
'',
);
const isRestrictedPath =
!isPlatform &&
nhostRoutes.some((route) =>
pathWithoutWorkspaceAndProject.startsWith(
route.relativeMainPath || route.relativePath,
),
);
// TODO(orgs) 1
// useNotFoundRedirect();
useEffect(() => {
if (isPlatform || !router.isReady) {
return;
}
if (isRestrictedPath) {
router.push('/local/local');
}
}, [isPlatform, isRestrictedPath, router]);
if (isRestrictedPath || loading) {
return <LoadingScreen />;
}
if (error) {
throw error;
}
if (!isPlatform) {
return (
<Box
component="main"
className={twMerge(
'relative flex-auto overflow-y-auto',
mainContainerClassName,
)}
{...mainContainerProps}
>
{children}
<NextSeo title="Local App" />
</Box>
);
}
return (
<Box
component="main"
className={twMerge(
'relative flex-auto overflow-y-auto',
mainContainerClassName,
)}
{...mainContainerProps}
>
{children}
<NextSeo title={project?.name} />
</Box>
);
}
/**
* This components wraps the content in an `AuthenticatedLayout` and fetches
* project and workspace data from the API. Use this layout for pages where
* project related data is necessary (e.g: Overview, Data Browser, etc.).
*/
export default function ProjectLayout({
children,
mainContainerProps,
...props
}: ProjectLayoutProps) {
return (
<AuthenticatedLayout {...props}>
<ProjectLayoutContent mainContainerProps={mainContainerProps}>
{children}
</ProjectLayoutContent>
</AuthenticatedLayout>
);
}

View File

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

View File

@@ -1,69 +0,0 @@
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import type { ProjectLayoutProps } from '@/features/orgs/layout/ProjectLayout';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useTheme } from '@mui/material';
import { twMerge } from 'tailwind-merge';
export interface SettingsLayoutProps extends ProjectLayoutProps {}
export default function SettingsLayout({ children }: SettingsLayoutProps) {
const theme = useTheme();
const { currentProject } = useCurrentWorkspaceAndProject();
const hasGitRepo = !!currentProject?.githubRepository;
return (
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex w-full flex-auto flex-col overflow-y-auto overflow-x-hidden"
>
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex h-full flex-col"
>
<RetryableErrorBoundary>
<div className="flex flex-col space-y-2">
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row place-content-center gap-2"
>
<Text color="warning" className="text-sm">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code
className={twMerge(
'rounded-md px-2 py-px',
theme.palette.mode === 'dark'
? 'bg-brown text-copper'
: 'bg-slate-200 text-slate-700',
)}
>
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository
and you only want these changes to apply to a subset of them,
please check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/guides/cli/configuration-overlays#configuration-overlays"
>
Configuration Overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
</div>
{children}
</RetryableErrorBoundary>
</Box>
</Box>
);
}

View File

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

View File

@@ -1,266 +0,0 @@
import { NavLink } from '@/components/common/NavLink';
import { Backdrop } from '@/components/ui/v2/Backdrop';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { SlidersIcon } from '@/components/ui/v2/icons/SlidersIcon';
import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface SettingsSidebarProps extends Omit<BoxProps, 'children'> {}
interface SettingsNavLinkProps 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.
*
* @default true
*/
exact?: boolean;
/**
* Class name passed to the text element.
*/
textClassName?: string;
}
function SettingsNavLink({
exact = true,
href,
children,
textClassName,
...props
}: SettingsNavLinkProps) {
const router = useRouter();
const baseUrl = `/${router.query.workspaceSlug}/${router.query.appSlug}/settings`;
const finalUrl = href && href !== '/' ? `${baseUrl}${href}` : baseUrl;
const active = exact
? router.asPath === finalUrl
: router.asPath.startsWith(finalUrl);
return (
<ListItem.Root>
<ListItem.Button
dense
href={finalUrl}
component={NavLink}
selected={active}
{...props}
>
<ListItem.Text className={textClassName}>{children}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
);
}
export default function SettingsSidebar({
className,
...props
}: SettingsSidebarProps) {
const isPlatform = useIsPlatform();
const [expanded, setExpanded] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
function toggleExpanded() {
setExpanded(!expanded);
}
function handleSelect() {
setExpanded(false);
}
function closeSidebarWhenEscapeIsPressed(event: KeyboardEvent) {
if (event.key === 'Escape') {
setExpanded(false);
}
}
useEffect(() => {
if (typeof document !== 'undefined') {
document.addEventListener('keydown', closeSidebarWhenEscapeIsPressed);
}
return () =>
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
}, []);
if (!currentProject) {
return null;
}
return (
<>
<Backdrop
open={expanded}
className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
role="button"
tabIndex={-1}
onClick={() => setExpanded(false)}
aria-label="Close sidebar overlay"
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
setExpanded(false);
}}
/>
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] flex h-full w-full flex-col justify-between overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:pb-0 md:pt-2.5 md:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
className,
)}
{...props}
>
<nav aria-label="Settings navigation" className="px-2">
<List className="grid gap-2">
<SettingsNavLink
href="/general"
exact={false}
onClick={handleSelect}
>
General
</SettingsNavLink>
<SettingsNavLink
href="/resources"
exact={false}
onClick={handleSelect}
>
Compute Resources
</SettingsNavLink>
<SettingsNavLink
href="/database"
exact={false}
onClick={handleSelect}
>
Database
</SettingsNavLink>
<SettingsNavLink
href="/hasura"
exact={false}
onClick={handleSelect}
>
Hasura
</SettingsNavLink>
<SettingsNavLink
href="/authentication"
exact={false}
onClick={handleSelect}
>
Authentication
</SettingsNavLink>
<SettingsNavLink
href="/sign-in-methods"
exact={false}
onClick={handleSelect}
>
Sign-In Methods
</SettingsNavLink>
<SettingsNavLink
href="/storage"
exact={false}
onClick={handleSelect}
>
Storage
</SettingsNavLink>
<SettingsNavLink
href="/roles-and-permissions"
exact={false}
onClick={handleSelect}
>
Roles and Permissions
</SettingsNavLink>
<SettingsNavLink href="/smtp" exact={false} onClick={handleSelect}>
SMTP
</SettingsNavLink>
<SettingsNavLink
href="/git"
exact={false}
onClick={handleSelect}
disabled={!isPlatform}
>
Git
</SettingsNavLink>
<SettingsNavLink
href="/environment-variables"
exact={false}
onClick={handleSelect}
>
Environment Variables
</SettingsNavLink>
<SettingsNavLink
href="/secrets"
exact={false}
onClick={handleSelect}
>
Secrets
</SettingsNavLink>
<SettingsNavLink
href="/custom-domains"
exact={false}
onClick={handleSelect}
>
Custom Domains
</SettingsNavLink>
<SettingsNavLink
href="/rate-limiting"
exact={false}
onClick={handleSelect}
>
Rate Limiting
</SettingsNavLink>
<SettingsNavLink href="/ai" exact={false} onClick={handleSelect}>
AI
</SettingsNavLink>
</List>
</nav>
<Box className="border-t">
<SettingsNavLink
href="/editor"
exact={false}
onClick={handleSelect}
className="flex w-full border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
textClassName="flex w-full justify-center"
>
<div className="flex w-full flex-row items-center justify-center space-x-4 py-2.5">
<SlidersIcon />
<span className="flex">Configuration Editor</span>
</div>
</SettingsNavLink>
</Box>
</Box>
<IconButton
className="absolute bottom-4 left-4 z-[38] h-11 w-11 rounded-full md:hidden"
onClick={toggleExpanded}
aria-label="Toggle sidebar"
>
<Image
width={16}
height={16}
src="/assets/table.svg"
alt="A monochrome table"
/>
</IconButton>
</>
);
}

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