Compare commits

..

24 Commits

Author SHA1 Message Date
github-actions[bot]
0fdff345ac chore: update versions (#3327)
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.29.0

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit
-   a0931e2: fix: improve logs time range and filter selection
- c0635ae: feat (dashboard): Add information about that free
organization cannot be upgraded.
- e87505c: fix: can downsize postgres storage capacity using local
dashboard

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

## @nhost-examples/sveltekit@0.8.0

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

### Patch Changes

-   97db637: fix: fix settings

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

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

### Minor Changes

-   c97b43f: fix: update vite to address vulnerability audit

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-05-12 20:05:18 +00:00
David Barroso
97db63791b fix (examples/react-apollo): fix settings (#3301)
### **PR Type**
Enhancement, Configuration changes


___

### **Description**
- Update auth settings in nhost.toml

- Remove rate limiting configurations

- Remove SMTP provider settings

- Add changeset for patch 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>lazy-years-kneel.md</strong><dd><code>Add changeset for
patch version bump</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>

.changeset/lazy-years-kneel.md

<li>Add new changeset file for patch version bump<br> <li> Specify
'@nhost-examples/react-apollo' package<br> <li> Include fix description


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3301/files#diff-019ff1fe3f65a591a01a6cbc0fa05c44c065e2611417ac0b3dcf6bd5a6eda7c2">+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 Nhost configuration
settings</code>&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>

examples/react-apollo/nhost/nhost.toml

<li>Change auth.elevatedPrivileges.mode to 'recommended'<br> <li> Remove
auth.rateLimit configurations<br> <li> Remove provider.smtp settings<br>
<li> Update allowed URLs for auth redirections


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3301/files#diff-268d6c8dddd6990d60d62c1c923955c4e0e7549a80f0f5856192f889378416a0">+1/-30</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-05-12 19:30:59 +02:00
David BM
a0931e282f fix (dashboard): logs persist time range selector, fix validation (#3300)
### **User description**
Resolves #3154


___

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


___

### **Description**
- Fix logs search functionality with selected service filter

- Implement persistent time range selector

- Add interval-based date range selection

- Improve form validation and submission logic


___



### **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>LogsDatePicker.tsx</strong><dd><code>Enhance
LogsDatePicker with form context integration</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsDatePicker/LogsDatePicker.tsx

<li>Import LogsFilterFormValues type and useFormContext hook<br> <li>
Add setValue function from form context<br> <li> Implement
handleDateChange to update selected date and reset interval<br> <li>
Update DatePicker onChange to use new handleDateChange function


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3300/files#diff-0768cb2a5cee4ab57a64580c34213950a042a9893b5da51b8886e166cb7a9060">+9/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td>
    <details>
<summary><strong>LogsHeader.tsx</strong><dd><code>Improve LogsHeader
with interval support and form handling</code></dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsHeader/LogsHeader.tsx

<li>Add interval to validation schema and form default values<br> <li>
Implement interval-based date recalculation in handleSubmit<br> <li>
Update form mode to 'onChange' for real-time validation<br> <li> Modify
useEffect to trigger submission on service change


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>LogsRangeSelector.tsx</strong><dd><code>Enhance
LogsRangeSelector with interval selection and UI
updates</code></dd></summary>
<hr>


dashboard/src/features/orgs/projects/logs/components/LogsRangeSelector/LogsRangeSelector.tsx

<li>Add interval handling in LogsToDatePickerLiveButton<br> <li>
Implement interval setting in handleIntervalChange function<br> <li>
Update Button variant based on selected interval<br> <li> Add interval
to useWatch hook for reactive updates


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3300/files#diff-46dd7c795a79e4b443213ed10089651423d13e5c776ca72e3a95ae5e0f7f63c8">+9/-2</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-05-12 18:31:58 +02:00
David BM
e87505c564 fix (dashboard): downsize postgres using local dashboard (#3292)
### **User description**
Fixes #3265


___

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


___

### **Description**
- Fix PostgreSQL version parsing for empty strings

- Enable downsizing Postgres in local dashboard

- Improve handling of free project and platform checks

- Update refetch queries with optional chaining


___



### **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>useGetPostgresVersion.ts</strong><dd><code>Fix
PostgreSQL version parsing for empty strings</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/database/common/hooks/useGetPostgresVersion/useGetPostgresVersion.ts

<li>Add fallback to empty string for <code>version</code> in
<br><code>splitPostgresMajorMinorVersions</code>


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>index.tsx</strong><dd><code>Update refetch queries with
optional chaining</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/pages/orgs/[orgSlug]/projects/[appSubdomain]/settings/index.tsx

- Add optional chaining for `userData.id` in refetch queries


</details>


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

</tr>
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>DatabaseStorageCapacity.tsx</strong><dd><code>Improve
database storage capacity handling and UI</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/database/settings/components/DatabaseStorageCapacity/DatabaseStorageCapacity.tsx

<li>Add <code>isEmptyValue</code> check for <code>org</code> object<br>
<li> Introduce <code>shouldShowUpdateCapacityWarning</code> variable<br>
<li> Modify <code>submitDisabled</code> logic for non-platform
environments<br> <li> Update conditional rendering of
<code>DatabaseStorageCapacityWarning</code>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3292/files#diff-097a59d13b44816051386182a444eadfe2dcacd69b88c121af6733d7eca3ee43">+16/-3</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-05-08 14:02:09 +02:00
robertkasza
c0635ae1c7 feat (dashboard): Add information about that free organization cannot be upgraded (#3316)
### **PR Type**
Enhancement


___

### **Description**
- Add info about free org upgrade limitations

- Introduce new 'NewOrgButton' component

- Update UI for subscription plan section

- Improve text link component functionality


___



### **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>InfoAlert.tsx</strong><dd><code>Enhance AlertTitle
styling</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></summary>
<hr>

dashboard/src/features/orgs/components/InfoAlert/InfoAlert.tsx

- Added 'font-semibold' class to AlertTitle


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>SubscriptionPlan.tsx</strong><dd><code>Update
subscription plan UI and add free org info</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/components/billing/components/SubscriptionPlan/SubscriptionPlan.tsx

<li>Added InfoAlert for free organizations<br> <li> Introduced
NewOrgButton component<br> <li> Updated layout and styling of
subscription plan section<br> <li> Replaced Link component with TextLink


</details>


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

</tr>

<tr>
  <td>
    <details>
<summary><strong>TextLink.tsx</strong><dd><code>Enhance TextLink
component with optional icon</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/orgs/projects/common/components/TextLink/TextLink.tsx

<li>Added optional icon to TextLink component<br> <li> Introduced
withIcon prop for flexibility


</details>


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

</tr>
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>good-frogs-share.md</strong><dd><code>Add changeset for
dashboard feature</code>&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/good-frogs-share.md

- Added changeset file for version bump


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3316/files#diff-ff1c12916da9254a5d59fef39d5220a0ccdd20a7e66e1436a860da9a014d31ee">+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-05-07 13:31:01 +02:00
robertkasza
d2a9a9ae1d fix: update labeler config (#3328)
### **PR Type**
Enhancement


___

### **Description**
- Update labeler configuration for all categories

- Standardize format using 'any' key for all rules

- Remove redundant 'any' key for documentation category

- Maintain existing category definitions and file patterns


___



### **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>labeler.yml</strong><dd><code>Standardize and simplify
labeler configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/labeler.yml

<li>Standardized all rules using 'any' key<br> <li> Removed redundant
'any' key for documentation<br> <li> Maintained existing category
definitions and file patterns<br> <li> Simplified overall configuration
structure


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/3328/files#diff-a22c263686553013feaeb0677d26eeb0b8778a756c4311c1fce13384258026aa">+8/-9</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-05-07 10:00:34 +02:00
David BM
c97b43f149 fix (ci): update vite to solve vulnerability audit (#3323)
### **PR Type**
Bug fix, Enhancement


___

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

- Upgrade Vite in multiple project dependencies

- Update package overrides for security

- Add changeset for minor version bumps


___



### **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>chatty-kids-exist.md</strong><dd><code>Add changeset for
minor version updates</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/3323/files#diff-22d27113acb695bcdab878d71e0e553a23f87070faeb4672ce09bf2108c56064">+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 to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-2d8d55c799cd71f1b35e831f075f8178ed1734c4820a2ad548b4dd24d6938d7c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-83675898dc6ed88838763232d022f6e100e07d71681cc8a1f02aee99ee3f229b">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-9fb3a23f389ab1d192d7e018d2acbe512bd8792278662101401caa98692735db">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-cb7094614884e8cd2c8fb67dadedb1887c46c31b888840def0b7042273bfbb28">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v6.2.7</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; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-6288951fff74ec246c9cc023b7b7e3e9aad31423891bc4ea25b5d84a5f5b061f">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-d95dc3391741287366ea2e61f70e9ccc64452e0d22b1db91d6bf524f5aa4331c">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-8a3e5ed0f618f15211c31f700e0da998e2eae58f60353624b7a7e637bd63b153">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-fc4298d3512fdd9a3d871f9f182fe871c8beccd1580f864a271ddfb32005feef">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite to
v5.4.19</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; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/nhost/nhost/pull/3323/files#diff-85166d1137e29a5275f991e1e94a0c9d5b83ac7504463ba76f9187b2b750c895">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Update Vite and adjust
overrides</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/3323/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519">+4/-4</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 <robert.kasza@bishop-co.com>
2025-05-06 14:39:39 +02:00
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
146 changed files with 2625 additions and 1080 deletions

19
.github/labeler.yml vendored
View File

@@ -1,24 +1,25 @@
dashboard:
- dashboard/**/*
- any:
- changed-files:
- any-glob-to-any-file: ['dashboard/**/*']
documentation:
- any:
- docs/**/*
- any: ['docs/**/*']
examples:
- examples/**/*
- any: ['examples/**/*']
sdk:
- packages/**/*
- any: ['packages/**/*']
integrations:
- integrations/**/*
- any: ['integrations/**/*']
react:
- '{packages,examples,integrations}/*react*/**/*'
- any: ['{packages,examples,integrations}/*react*/**/*']
nextjs:
- '{packages,examples}/*next*/**/*'
- any: ['{packages,examples}/*next*/**/*']
vue:
- '{packages,examples,integrations}/*vue*/**/*'
- any: ['{packages,examples,integrations}/*vue*/**/*']

View File

@@ -7,12 +7,14 @@ on:
- 'assets/**'
- '**.md'
- 'LICENSE'
- 'docs/**'
pull_request:
types: [opened, synchronize]
paths-ignore:
- 'assets/**'
- '**.md'
- 'LICENSE'
- 'docs/**'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
@@ -27,7 +29,8 @@ env:
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
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:
build:
@@ -169,6 +172,10 @@ jobs:
- name: Set Dashboard 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
- 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
- name: Run e2e tests
timeout-minutes: 20
@@ -177,13 +184,16 @@ jobs:
- name: Run Local Dashboard e2e tests
if: matrix.package.path == 'dashboard'
timeout-minutes: 5
run: |
pnpm --filter="${{ matrix.package.name }}" run e2e-local
run: pnpm --filter="${{ matrix.package.name }}" run e2e:local
- name: Stop Nhost CLI
if: matrix.package.path == 'dashboard'
working-directory: ./nhost-test-project
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
if: ${{ failure() }}
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 build --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

@@ -3,13 +3,12 @@ on:
- pull_request_target
jobs:
triage:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
- uses: actions/labeler@v5
with:
repo-token: '${{ secrets.GH_PAT }}'
sync-labels: ''
repo-token: ${{ secrets.GH_PAT }}

View File

@@ -1,5 +1,31 @@
# @nhost/dashboard
## 2.29.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
- a0931e2: fix: improve logs time range and filter selection
- c0635ae: feat (dashboard): Add information about that free organization cannot be upgraded.
- e87505c: fix: can downsize postgres storage capacity using local 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

View File

@@ -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_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,49 @@
/* eslint-disable no-console */
import { TEST_PROJECT_ADMIN_SECRET, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import { test as setup } from '@playwright/test';
setup('refresh metadata', async () => {
try {
const response = 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',
}),
},
);
const body = await response.json();
if (!response.ok) {
const message = `[${body.code}]:${body.error}`;
console.log(message);
throw new Error(message);
} else {
console.log('Metadata is consistent.');
}
} 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

@@ -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,6 +1,12 @@
import { TEST_ORGANIZATION_SLUG, TEST_PROJECT_SUBDOMAIN } from '@/e2e/env';
import {
TEST_FREE_USER_EMAILS,
TEST_ORGANIZATION_SLUG,
TEST_PROJECT_SUBDOMAIN,
TEST_USER_PASSWORD,
} from '@/e2e/env';
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.
@@ -213,8 +219,96 @@ export async function clickPermissionButton({
.click();
}
export async function gotoAuthURL(page) {
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",
"version": "2.27.0",
"version": "2.29.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -16,8 +16,10 @@
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e": "pnpm install-browsers && pnpm playwright test --config=playwright.config.ts",
"e2e-local": "pnpm install-browsers && pnpm playwright test --config=playwright.local.config.ts"
"e2e:tests": "pnpm install-browsers && pnpm playwright test --config=playwright.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": {
"@apollo/client": "^3.9.9",
@@ -87,7 +89,7 @@
"just-kebab-case": "^4.2.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.416.0",
"next": "^14.2.25",
"next": "^14.2.26",
"next-nprogress-bar": "^2.3.13",
"next-seo": "^6.5.0",
"next-themes": "^0.3.0",
@@ -194,7 +196,7 @@
"tailwindcss": "^3.4.12",
"ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"vite": "^5.4.17",
"vite": "^5.4.19",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^0.32.4"
},

View File

@@ -17,7 +17,7 @@ export default defineConfig({
reporter: 'html',
use: {
actionTimeout: 0,
trace: 'on-first-retry',
trace: 'retain-on-failure',
baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
launchOptions: {
slowMo: 500,
@@ -34,13 +34,28 @@ export default defineConfig({
testMatch: ['**/teardown/*.teardown.ts'],
},
{
name: 'chromium',
name: 'main',
use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/user.json',
},
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

@@ -82,7 +82,7 @@ describe('DateTimePicker', () => {
const secondsInput = await screen.getByLabelText('Seconds');
await user.type(secondsInput, '13');
user.click(await screen.getByRole('button', { name: 'Select' }));
await user.click(await screen.getByRole('button', { name: 'Select' }));
await waitFor(async () =>
expect(

View File

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

View File

@@ -3,7 +3,7 @@ import { Box } from '@/components/ui/v2/Box';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
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 { 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">
<OpenTransferDialogButton onClick={handleTransferDialogOpen} />
<TransferProjectDialog
<TransferOrUpgradeProjectDialog
open={transferProjectDialogOpen}
setOpen={setTransferProjectDialogOpen}
/>

View File

@@ -26,7 +26,7 @@ function InfoAlert({
<Alert className={alertClassNames}>
{icon && <div>{icon}</div>}
<div>
{title && <AlertTitle>{title}</AlertTitle>}
{title && <AlertTitle className="font-semibold">{title}</AlertTitle>}
{children && (
<AlertDescription className={descClassNames}>
{children}

View File

@@ -14,13 +14,15 @@ export default function StripeEmbeddedForm({
clientSecret: string;
}) {
return (
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{
clientSecret,
}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
<div className="h-[80vh] overflow-y-scroll">
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{
clientSecret,
}}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { useUI } from '@/components/common/UIProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
import { TransferOrUpgradeProjectDialog } from '@/features/orgs/components/common/TransferOrUpgradeProjectDialog';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useState } from 'react';
@@ -26,7 +26,7 @@ export default function TransferProject() {
}}
/>
<TransferProjectDialog open={open} setOpen={setOpen} />
<TransferOrUpgradeProjectDialog open={open} setOpen={setOpen} />
</>
);
}

View File

@@ -1,7 +1,6 @@
import { useUI } from '@/components/common/UIProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Button } from '@/components/ui/v3/button';
import {
Dialog,
@@ -21,6 +20,8 @@ import {
FormMessage,
} from '@/components/ui/v3/form';
import { RadioGroup, RadioGroupItem } from '@/components/ui/v3/radio-group';
import { InfoAlert } from '@/features/orgs/components/InfoAlert';
import TextLink from '@/features/orgs/projects/common/components/TextLink/TextLink';
import { planDescriptions } from '@/features/orgs/projects/common/utils/planDescriptions';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
@@ -35,6 +36,14 @@ import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
function NewOrgButton() {
return (
<strong className="inline-flex items-center justify-center gap-2 px-1">
<span>+ New Organization</span>
</strong>
);
}
const changeOrgPlanForm = z.object({
plan: z.string(),
});
@@ -48,6 +57,8 @@ export default function SubscriptionPlan() {
const [fetchOrganizationCustomePortalLink, { loading }] =
useBillingOrganizationCustomePortalLazyQuery();
const isFreeOrg = org?.plan.isFree;
const form = useForm<z.infer<typeof changeOrgPlanForm>>({
resolver: zodResolver(changeOrgPlanForm),
defaultValues: {
@@ -125,7 +136,7 @@ export default function SubscriptionPlan() {
<div className="flex w-full flex-col gap-1 border-b p-4">
<h4 className="font-medium">Subscription plan</h4>
</div>
<div className="flex w-full flex-col justify-between gap-8 border-b p-4 md:flex-row">
<div className="flex w-full flex-col justify-between gap-8 p-4 md:flex-row">
<div className="flex basis-1/2 flex-col gap-4">
<span className="font-medium">Organization name</span>
<span className="font-medium">{org?.name}</span>
@@ -152,31 +163,26 @@ export default function SubscriptionPlan() {
</div>
</div>
</div>
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 p-4 md:flex-row md:items-center md:gap-0">
{isFreeOrg && (
<div className="flex w-full flex-col justify-between gap-8 p-4 md:flex-row">
<InfoAlert title="Personal Organizations can not be upgraded.">
You may create a new organization with premium features by
clicking on the <NewOrgButton /> button in the left sidebar.
</InfoAlert>
</div>
)}
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 border-t p-4 md:flex-row md:items-center md:gap-0">
<div>
<span>For a complete list of features, visit our </span>
<Link
href="https://nhost.io/pricing"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="https://nhost.io/pricing">
pricing
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
</Link>
</TextLink>
<span> You can also visit our </span>
<Link
href="https://docs.nhost.io/platform/cloud/billing"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="https://docs.nhost.io/platform/cloud/billing">
documentation
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
</Link>
</TextLink>
<span> for billing information</span>
</div>
<div className="flex w-full flex-row items-center justify-end gap-2">
@@ -245,7 +251,7 @@ export default function SubscriptionPlan() {
</div>
<div className="mt-0 flex h-full items-center text-xl font-semibold">
{plan.isFree ? 'Free' : `${plan.price}/mo`}
{isFreeOrg ? 'Free' : `${plan.price}/mo`}
</div>
</FormLabel>
</FormItem>
@@ -264,16 +270,10 @@ export default function SubscriptionPlan() {
</div>
</div>
<Link
href="mailto:hello@nhost.io"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
<TextLink href="mailto:hello@nhost.io">
Contact us
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</TextLink>
</div>
</div>
</RadioGroup>

View File

@@ -1,24 +1,30 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { DialogDescription } from '@/components/ui/v3/dialog';
import { useFinishOrgCreation } from '@/features/orgs/hooks/useFinishOrgCreation';
import { type FinishOrgCreationOnCompletedCb } from '@/features/orgs/hooks/useFinishOrgCreation/useFinishOrgCreation';
import { CheckoutStatus } from '@/utils/__generated__/graphql';
import { memo } from 'react';
interface Props {
loading: boolean;
status: CheckoutStatus | null;
onCompleted: FinishOrgCreationOnCompletedCb;
onError?: () => void;
successMessage: string;
loadingMessage: string;
errorMessage: string;
pendingMessage: string;
withDialogDescription?: boolean;
}
function FinishOrgCreationProcess({
loading,
status,
onCompleted,
onError,
successMessage,
loadingMessage,
errorMessage,
pendingMessage,
withDialogDescription,
}: Props) {
const [loading, status] = useFinishOrgCreation({ onCompleted, onError });
let message: string | undefined;
switch (status) {
@@ -38,13 +44,15 @@ function FinishOrgCreationProcess({
message = loadingMessage;
}
const Component = withDialogDescription ? DialogDescription : 'span';
return (
<div className="relative flex flex-auto overflow-x-hidden">
<div className="flex h-full w-full flex-col items-center justify-center space-y-2">
{(loading || status === CheckoutStatus.Completed) && (
<ActivityIndicator circularProgressProps={{ className: 'w-6 h-6' }} />
)}
<span>{message}</span>
<Component>{message}</Component>
</div>
</div>
);

View File

@@ -1,5 +1,4 @@
import { FinishOrgCreationProcess } from '@/features/orgs/components/common/FinishOrgCreationProcess';
import { useFinishOrgCreation } from '@/features/orgs/hooks/useFinishOrgCreation';
import { type FinishOrgCreationOnCompletedCb } from '@/features/orgs/hooks/useFinishOrgCreation/useFinishOrgCreation';
interface Props {
@@ -8,15 +7,15 @@ interface Props {
}
function FinishOrgCreation({ onCompleted, onError }: Props) {
const [loading, status] = useFinishOrgCreation({ onCompleted, onError });
return (
<FinishOrgCreationProcess
loading={loading}
status={status}
loadingMessage="Processing new organization request"
onCompleted={onCompleted}
onError={onError}
loadingMessage="Creating new organization"
successMessage="Organization created successfully."
pendingMessage="Organization creation is pending..."
errorMessage="Error occurred while creating the organization. Please try again."
withDialogDescription
/>
);
}

View File

@@ -22,12 +22,17 @@ import {
import { setupServer } from 'msw/node';
import { useState } from 'react';
import { afterAll, beforeAll, vi } from 'vitest';
import TransferProjectDialog from './TransferProjectDialog';
import TransferorUpgradeProjectDialog from './TransferOrUpgradeProjectDialog';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(mockMatchMediaValue),
});
const mocks = vi.hoisted(() => ({
useRouter: vi.fn(),
useOrgs: vi.fn(),
push: vi.fn(),
}));
mockPointerEvent();
@@ -44,7 +49,7 @@ const getUseRouterObject = (session_id?: string) => ({
appSubdomain: 'test-project',
session_id,
},
push: vi.fn(),
push: mocks.push,
replace: vi.fn(),
reload: vi.fn(),
back: vi.fn(),
@@ -58,11 +63,6 @@ const getUseRouterObject = (session_id?: string) => ({
isFallback: false,
});
const mocks = vi.hoisted(() => ({
useRouter: vi.fn(),
useOrgs: vi.fn(),
}));
vi.mock('@/features/orgs/projects/hooks/useOrgs', async () => {
const actualUseOrgs = await vi.importActual<any>(
'@/features/orgs/projects/hooks/useOrgs',
@@ -78,17 +78,42 @@ const postOrganizationRequestResolver = createGraphqlMockResolver(
'mutation',
);
const billingTransferAppRequestResolver = createGraphqlMockResolver(
'billingTransferApp',
'mutation',
);
vi.mock('next/router', () => ({
useRouter: mocks.useRouter,
}));
async function asyncFireEvent(element: Document | Element | Window | Node) {
await waitFor(() => {
fireEvent(
element,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
});
}
export function DialogWrapper({
defaultOpen = true,
isUpgrade = false,
}: {
defaultOpen?: boolean;
isUpgrade?: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
return <TransferProjectDialog open={open} setOpen={setOpen} />;
return (
<TransferorUpgradeProjectDialog
open={open}
setOpen={setOpen}
isUpgrade={isUpgrade}
/>
);
}
const server = setupServer(tokenQuery);
@@ -102,11 +127,12 @@ beforeAll(() => {
afterEach(() => {
queryClient.clear();
mocks.useRouter.mockRestore();
mocks.push.mockRestore();
vi.restoreAllMocks();
});
afterAll(() => {
server.close();
vi.restoreAllMocks();
});
test('opens create org dialog when selecting "create new org" and closes transfer dialog', async () => {
@@ -141,13 +167,7 @@ test('opens create org dialog when selecting "create new org" and closes transfe
const submitButton = await screen.findByText('Continue');
expect(submitButton).toHaveTextContent('Continue');
fireEvent(
submitButton,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
asyncFireEvent(submitButton);
await waitFor(() => {
expect(submitButton).not.toBeInTheDocument();
@@ -156,13 +176,48 @@ test('opens create org dialog when selecting "create new org" and closes transfe
const newOrgTitle = await screen.findByText('New Organization');
expect(newOrgTitle).toBeInTheDocument();
const closeButton = await screen.findByText('Close');
fireEvent(
closeButton,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
asyncFireEvent(closeButton);
await waitFor(() => {
expect(newOrgTitle).not.toBeInTheDocument();
});
const submitButtonAfterClosingNewOrgDialog =
await screen.findByText('Continue');
await waitFor(() => {
expect(submitButtonAfterClosingNewOrgDialog).toHaveTextContent('Continue');
});
});
test('when upgrading a project by clicking on the Continue button the create new org modal is opened and the initial dialog is closed', async () => {
mocks.useRouter.mockImplementation(() => getUseRouterObject());
server.use(getProjectQuery);
server.use(getOrganization);
mocks.useOrgs.mockImplementation(() => ({
orgs: mockOrganizations,
currentOrg: mockOrganization,
loading: false,
refetch: vi.fn(),
}));
server.use(prefetchNewAppQuery);
render(<DialogWrapper isUpgrade />);
expect(await screen.findByText('Upgrade project')).toBeInTheDocument();
const submitButton = await screen.findByText('Continue');
expect(submitButton).toHaveTextContent('Continue');
asyncFireEvent(submitButton);
await waitFor(() => {
expect(submitButton).not.toBeInTheDocument();
});
const newOrgTitle = await screen.findByText('New Organization');
expect(newOrgTitle).toBeInTheDocument();
const closeButton = await screen.findByText('Close');
asyncFireEvent(closeButton);
await waitFor(() => {
expect(newOrgTitle).not.toBeInTheDocument();
});
@@ -182,7 +237,9 @@ test(`transfer dialog opens automatically when there is a session_id and selects
orgs: mockOrganizations,
currentOrg: mockOrganization,
loading: false,
refetch: vi.fn(),
refetch: async () => ({
data: { organizations: mockOrganizationsWithNewOrg },
}),
}));
server.use(prefetchNewAppQuery);
server.use(postOrganizationRequestResolver.handler);
@@ -196,13 +253,7 @@ test(`transfer dialog opens automatically when there is a session_id and selects
const closeButton = await screen.findByText('Close');
fireEvent(
closeButton,
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
asyncFireEvent(closeButton);
await waitFor(() => {});
expect(closeButton).toBeInTheDocument();
@@ -220,15 +271,84 @@ test(`transfer dialog opens automatically when there is a session_id and selects
orgs: mockOrganizationsWithNewOrg,
currentOrg: mockOrganization,
loading: false,
refetch: vi.fn(),
refetch: async () => ({
data: { organizations: mockOrganizationsWithNewOrg },
}),
}));
await waitFor(async () => {
expect(
await screen.queryByRole('combobox', {
name: /Organization/i,
}),
).toBeInTheDocument();
});
const organizationCombobox = await screen.findByRole('combobox', {
name: /Organization/i,
});
expect(organizationCombobox).toHaveTextContent(newOrg.name);
const submitButton = await screen.findByText('Transfer');
expect(submitButton).not.toBeDisabled();
});
test(`upgrade project dialog opens automatically when there is a session_id and transfers the project to ${newOrg.name}`, async () => {
mocks.useRouter.mockImplementation(() => getUseRouterObject('session_id'));
server.use(getProjectQuery);
server.use(getOrganization);
mocks.useOrgs.mockImplementation(() => ({
orgs: mockOrganizations,
currentOrg: mockOrganization,
loading: false,
refetch: async () => ({
data: { organizations: mockOrganizationsWithNewOrg },
}),
}));
server.use(prefetchNewAppQuery);
server.use(postOrganizationRequestResolver.handler);
server.use(billingTransferAppRequestResolver.handler);
render(<DialogWrapper defaultOpen={false} isUpgrade />);
const processingNewOrgText = await screen.findByText(
'Creating new organization',
);
expect(processingNewOrgText).toBeInTheDocument();
const closeButton = await screen.findByText('Close');
asyncFireEvent(closeButton);
await waitFor(() => {});
expect(closeButton).toBeInTheDocument();
postOrganizationRequestResolver.resolve({
billingPostOrganizationRequest: {
Status: 'COMPLETED',
Slug: newOrg.slug,
ClientSecret: null,
__typename: 'PostOrganizationRequestResponse',
},
});
mocks.useOrgs.mockImplementation(() => ({
orgs: mockOrganizationsWithNewOrg,
currentOrg: mockOrganization,
loading: false,
refetch: async () => ({
data: { organizations: mockOrganizationsWithNewOrg },
}),
}));
await waitFor(async () => {
expect(await screen.findByText('Upgrading project...')).toBeInTheDocument();
});
billingTransferAppRequestResolver.resolve({
billingTransferApp: true,
});
await waitFor(async () => {});
expect(mocks.push).toHaveBeenCalledWith('/orgs/new-org/projects');
});

View File

@@ -0,0 +1,105 @@
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { Dialog, DialogContent } from '@/components/ui/v3/dialog';
import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import TransferProjectDialogContent from './TransferProjectDialogContent';
import UpgradeProjectDialogContent from './UpgradeProjectDialogContent';
interface TransferProjectDialogProps {
open: boolean;
setOpen: (value: boolean) => void;
isUpgrade?: boolean;
}
export default function TransferOrUpgradeProjectDialog({
open,
setOpen,
isUpgrade,
}: TransferProjectDialogProps) {
const { asPath, query, isReady: isRouterReady } = useRouter();
const { session_id } = query;
const { loading: projectLoading } = useProject();
const { loading: orgsLoading } = useOrgs();
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
const [preventClose, setPreventClose] = useState(false);
const [selectedOrgId, setSelectedOrgId] = useState<string>();
useEffect(() => {
if (session_id && isRouterReady) {
setOpen(true);
setPreventClose(true);
}
}, [session_id, setOpen, isRouterReady]);
const path = asPath.split('?')[0];
const redirectUrl = `${window.location.origin}${path}`;
const handleCreateDialogOpenStateChange = (newState: boolean) => {
setShowCreateOrgModal(newState);
setOpen(true);
};
const handleFinishOrgCreationCompleted = useCallback(async () => {
setPreventClose(false);
}, []);
const handleTransferProjectDialogOpenChange = (newValue: boolean) => {
if (preventClose) {
return;
}
if (!newValue) {
setSelectedOrgId(undefined);
}
setOpen(newValue);
};
const handleCancel = () => {
setOpen(false);
};
const handleCreateNewOrg = () => {
setShowCreateOrgModal(true);
setOpen(false);
};
const handleOnCreateOrgError = useCallback(() => setPreventClose(false), []);
if (projectLoading || orgsLoading) {
return <LoadingScreen />;
}
return (
<>
<Dialog open={open} onOpenChange={handleTransferProjectDialogOpenChange}>
<DialogContent className="z-[9999] text-foreground sm:max-w-xl">
{isUpgrade ? (
<UpgradeProjectDialogContent
onCancel={handleCancel}
onCreateNewOrg={handleCreateNewOrg}
onCreateOrgError={handleOnCreateOrgError}
/>
) : (
<TransferProjectDialogContent
onFinishOrgCreationCompleted={handleFinishOrgCreationCompleted}
onFinishOrgError={() => setPreventClose(false)}
onCreateNewOrg={handleCreateNewOrg}
onCancel={handleCancel}
selectedOrganizationId={selectedOrgId}
onOrganizationChange={setSelectedOrgId}
/>
)}
</DialogContent>
</Dialog>
<CreateOrgDialog
hideNewOrgButton
isOpen={showCreateOrgModal}
onOpenStateChange={handleCreateDialogOpenStateChange}
redirectUrl={redirectUrl}
/>
</>
);
}

View File

@@ -0,0 +1,100 @@
import {
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/v3/dialog';
import { type FinishOrgCreationOnCompletedCb } from '@/features/orgs/hooks/useFinishOrgCreation/useFinishOrgCreation';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { isNotEmptyValue } from '@/lib/utils';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import FinishOrgCreation from './FinishOrgCreation';
import TransferProjectForm, {
type TransferProjectFormProps,
} from './TransferProjectForm';
interface Props extends TransferProjectFormProps {
onFinishOrgCreationCompleted: () => void;
onFinishOrgError: () => void;
}
function TransferProjectDialogContent({
onCreateNewOrg,
onCancel,
onFinishOrgCreationCompleted,
onFinishOrgError,
selectedOrganizationId,
onOrganizationChange,
}: Props) {
const { query, replace, pathname } = useRouter();
const { session_id, ...remainingQuery } = query;
const { refetch: refetchOrgs } = useOrgs();
const [showContent, setShowContent] = useState(true);
const removeSessionIdFromQuery = useCallback(() => {
replace({ pathname, query: remainingQuery }, undefined, {
shallow: true,
});
}, [replace, remainingQuery, pathname]);
useEffect(() => {
if (isNotEmptyValue(session_id)) {
setShowContent(false);
}
}, [session_id]);
const handleOnCompleted: FinishOrgCreationOnCompletedCb = useCallback(
async ({ Slug }) => {
removeSessionIdFromQuery();
const {
data: { organizations },
} = await refetchOrgs();
const newOrg = organizations.find((org) => org.slug === Slug);
setShowContent(true);
onOrganizationChange(newOrg.id);
onFinishOrgCreationCompleted();
},
[
onFinishOrgCreationCompleted,
refetchOrgs,
removeSessionIdFromQuery,
onOrganizationChange,
],
);
return (
<>
<DialogHeader className="flex gap-2">
<DialogTitle>
Move the current project to a different organization.
</DialogTitle>
</DialogHeader>
{showContent ? (
<>
<DialogDescription>
To transfer a project between organizations, you must be an{' '}
<span className="font-bold">ADMIN</span> in both.
<br />
When transferred to a new organization, the project will adopt that
organizations plan.
</DialogDescription>
<TransferProjectForm
onCreateNewOrg={onCreateNewOrg}
selectedOrganizationId={selectedOrganizationId}
onCancel={onCancel}
onOrganizationChange={onOrganizationChange}
/>
</>
) : (
<FinishOrgCreation
onCompleted={handleOnCompleted}
onError={onFinishOrgError}
/>
)}
</>
);
}
export default TransferProjectDialogContent;

View File

@@ -0,0 +1,186 @@
import { Badge } from '@/components/ui/v3/badge';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/v3/select';
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { cn, isNotEmptyValue } from '@/lib/utils';
import {
Organization_Members_Role_Enum,
useBillingTransferAppMutation,
} from '@/utils/__generated__/graphql';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUserId } from '@nhost/nextjs';
import { Plus } from 'lucide-react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const CREATE_NEW_ORG = 'createNewOrg';
const transferProjectFormSchema = z.object({
organization: z.string(),
});
export interface TransferProjectFormProps {
onCreateNewOrg: () => void;
onCancel: () => void;
selectedOrganizationId?: string;
onOrganizationChange(value: string): void;
}
function TransferProjectForm({
onCreateNewOrg,
selectedOrganizationId,
onCancel,
onOrganizationChange,
}: TransferProjectFormProps) {
const { push } = useRouter();
const { orgs, currentOrg } = useOrgs();
const { project } = useProject();
const currentUserId = useUserId();
const [transferProject] = useBillingTransferAppMutation();
const form = useForm<z.infer<typeof transferProjectFormSchema>>({
resolver: zodResolver(transferProjectFormSchema),
defaultValues: {
organization: '',
},
});
useEffect(() => {
if (isNotEmptyValue(selectedOrganizationId)) {
form.setValue('organization', selectedOrganizationId, {
shouldDirty: true,
});
}
}, [selectedOrganizationId, form]);
const isUserAdminOfOrg = (org: Org, userId: string) =>
org.members.some(
(member) =>
member.role === Organization_Members_Role_Enum.Admin &&
member.user.id === userId,
);
const createNewFormSelected = form.watch('organization') === CREATE_NEW_ORG;
const submitButtonText = createNewFormSelected ? 'Continue' : 'Transfer';
const onSubmit = async (
values: z.infer<typeof transferProjectFormSchema>,
) => {
const { organization } = values;
if (organization === CREATE_NEW_ORG) {
onCreateNewOrg();
} else {
await execPromiseWithErrorToast(
async () => {
await transferProject({
variables: {
appID: project?.id,
organizationID: organization,
},
});
const targetOrg = orgs.find((o) => o.id === organization);
await push(`/orgs/${targetOrg.slug}/projects`);
},
{
loadingMessage: 'Transferring project...',
successMessage: 'Project transferred successfully!',
errorMessage: 'Error transferring project. Please try again.',
},
);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="organization"
render={({ field }) => (
<FormItem>
<FormLabel>Organization</FormLabel>
<Select onValueChange={onOrganizationChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Organization" />
</SelectTrigger>
</FormControl>
<SelectContent>
{orgs.map((org) => (
<SelectItem
key={org.id}
value={org.id}
disabled={
org.plan.isFree || // disable the personal org
org.id === currentOrg.id || // disable the current org as it can't be a destination org
!isUserAdminOfOrg(org, currentUserId) // disable orgs that the current user is not admin of
}
>
{org.name}
<Badge
variant={org.plan.isFree ? 'outline' : 'default'}
className={cn(
org.plan.isFree ? 'bg-muted' : '',
'hover:none ml-2 h-5 px-[6px] text-[10px]',
)}
>
{org.plan.name}
</Badge>
</SelectItem>
))}
<SelectItem key={CREATE_NEW_ORG} value={CREATE_NEW_ORG}>
<div className="flex items-center justify-center gap-2">
<Plus className="h-4 w-4 font-bold" strokeWidth={3} />{' '}
<span>New Organization</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2">
<Button
variant="secondary"
type="button"
disabled={form.formState.isSubmitting}
onClick={onCancel}
>
Cancel
</Button>
<Button
type="submit"
disabled={form.formState.isSubmitting || !form.formState.isDirty}
loading={form.formState.isSubmitting}
>
{submitButtonText}
</Button>
</div>
</form>
</Form>
);
}
export default TransferProjectForm;

View File

@@ -0,0 +1,116 @@
import { Button } from '@/components/ui/v3/button';
import {
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/v3/dialog';
import { type FinishOrgCreationOnCompletedCb } from '@/features/orgs/hooks/useFinishOrgCreation/useFinishOrgCreation';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { analytics } from '@/lib/segment';
import { isEmptyValue } from '@/lib/utils';
import { useBillingTransferAppMutation } from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
import { memo } from 'react';
import FinishOrgCreation from './FinishOrgCreation';
interface Props {
onCreateOrgError: () => void;
onCancel: () => void;
onCreateNewOrg: () => void;
}
function UpgradeProjectDialogContent({
onCreateNewOrg,
onCancel,
onCreateOrgError,
}: Props) {
const [transferProjectMutation] = useBillingTransferAppMutation();
const { project } = useProject();
const { refetch: refetchOrgs } = useOrgs();
const { push, query } = useRouter();
const { session_id } = query;
const showContent = isEmptyValue(session_id);
async function transferProject(newOrgSlug: string) {
const { data } = await refetchOrgs();
const newOrg = data.organizations.find((org) => org.slug === newOrgSlug);
await execPromiseWithErrorToast(
async () => {
await transferProjectMutation({
variables: {
appID: project?.id,
organizationID: newOrg?.id,
},
});
analytics.track('Project Upgraded', {
projectId: project?.id,
projectName: project?.name,
projectSubdomain: project?.subdomain,
newOrganizationId: newOrg?.id,
newOrganizationName: newOrg?.name,
newOrganizationSlug: newOrg?.slug,
newOrganizationPlan: newOrg?.plan?.name,
newOrganizationPlanId: newOrg?.plan?.id,
});
await push(`/orgs/${newOrg?.slug}/projects`);
},
{
loadingMessage: 'Upgrading project...',
successMessage: 'Project has been upgraded successfully!',
errorMessage: 'Error upgrading project. Please try again.',
},
);
}
const handleOnCompleted: FinishOrgCreationOnCompletedCb = async (data) => {
await transferProject(data.Slug);
};
return (
<>
<DialogHeader className="flex gap-2">
<DialogTitle>Upgrade project</DialogTitle>
</DialogHeader>
{showContent ? (
<>
<DialogDescription className="text-base">
<span className="mb-4 block">
To access premium features from a paid plan, a project must belong
to an organization on that plan.
</span>
<span className="mb-4 block">
Continue to create a new organization with a subscription plan.
Your project will be automatically transferred to the new
organization, unlocking all paid features.
</span>
<span className="block">
Alternatively, you can transfer your project to an existing paid
organization in your project&apos;s settings.
</span>
</DialogDescription>
<DialogFooter>
<Button variant="secondary" type="button" onClick={onCancel}>
Cancel
</Button>
<Button type="submit" onClick={onCreateNewOrg}>
Continue
</Button>
</DialogFooter>
</>
) : (
<FinishOrgCreation
onCompleted={handleOnCompleted}
onError={onCreateOrgError}
/>
)}
</>
);
}
export default memo(UpgradeProjectDialogContent);

View File

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

View File

@@ -1,306 +0,0 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/v3/dialog';
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Badge } from '@/components/ui/v3/badge';
import { Button } from '@/components/ui/v3/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/v3/form';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/v3/select';
import FinishOrgCreation from '@/features/orgs/components/common/TransferProjectDialog/FinishOrgCreation';
import CreateOrgDialog from '@/features/orgs/components/CreateOrgFormDialog/CreateOrgFormDialog';
import type { FinishOrgCreationOnCompletedCb } from '@/features/orgs/hooks/useFinishOrgCreation/useFinishOrgCreation';
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { cn, isNotEmptyValue } from '@/lib/utils';
import {
Organization_Members_Role_Enum,
useBillingTransferAppMutation,
} from '@/utils/__generated__/graphql';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUserId } from '@nhost/nextjs';
import { Plus } from 'lucide-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const CREATE_NEW_ORG = 'createNewOrg';
interface TransferProjectDialogProps {
open: boolean;
setOpen: (value: boolean) => void;
}
const transferProjectFormSchema = z.object({
organization: z.string(),
});
export default function TransferProjectDialog({
open,
setOpen,
}: TransferProjectDialogProps) {
const { push, asPath, query, replace, pathname } = useRouter();
const { session_id, test, ...remainingQuery } = query;
const currentUserId = useUserId();
const { project, loading: projectLoading } = useProject();
const {
orgs,
currentOrg,
loading: orgsLoading,
refetch: refetchOrgs,
} = useOrgs();
const [transferProject] = useBillingTransferAppMutation();
const [showCreateOrgModal, setShowCreateOrgModal] = useState(false);
const [finishOrgCreation, setFinishOrgCreation] = useState(false);
const [preventClose, setPreventClose] = useState(false);
const [newOrgSlug, setNewOrgSlug] = useState<string | undefined>();
const form = useForm<z.infer<typeof transferProjectFormSchema>>({
resolver: zodResolver(transferProjectFormSchema),
defaultValues: {
organization: '',
},
});
useEffect(() => {
if (session_id) {
setOpen(true);
setFinishOrgCreation(true);
setPreventClose(true);
}
}, [session_id, setOpen]);
useEffect(() => {
if (isNotEmptyValue(newOrgSlug)) {
const newOrg = orgs.find((org) => org.slug === newOrgSlug);
if (newOrg) {
form.setValue('organization', newOrg?.id, { shouldDirty: true });
}
}
}, [newOrgSlug, orgs, form]);
const createNewFormSelected = form.watch('organization') === CREATE_NEW_ORG;
const submitButtonText = createNewFormSelected ? 'Continue' : 'Transfer';
const path = asPath.split('?')[0];
const redirectUrl = `${window.location.origin}${path}`;
const handleCreateDialogOpenStateChange = (newState: boolean) => {
setShowCreateOrgModal(newState);
setOpen(true);
};
const onSubmit = async (
values: z.infer<typeof transferProjectFormSchema>,
) => {
const { organization } = values;
if (organization === CREATE_NEW_ORG) {
setShowCreateOrgModal(true);
setOpen(false);
} else {
await execPromiseWithErrorToast(
async () => {
await transferProject({
variables: {
appID: project?.id,
organizationID: organization,
},
});
const targetOrg = orgs.find((o) => o.id === organization);
await push(`/orgs/${targetOrg.slug}/projects`);
},
{
loadingMessage: 'Transferring project...',
successMessage: 'Project transferred successfully!',
errorMessage: 'Error transferring project. Please try again.',
},
);
}
};
const isUserAdminOfOrg = (org: Org, userId: string) =>
org.members.some(
(member) =>
member.role === Organization_Members_Role_Enum.Admin &&
member.user.id === userId,
);
const removeSessionIdFromQuery = () => {
replace({ pathname, query: remainingQuery }, undefined, { shallow: true });
};
const handleFinishOrgCreationCompleted: FinishOrgCreationOnCompletedCb =
async (data) => {
const { Slug } = data;
await refetchOrgs();
setNewOrgSlug(Slug);
setFinishOrgCreation(false);
removeSessionIdFromQuery();
setPreventClose(false);
};
const handleTransferProjectDialogOpenChange = (newValue: boolean) => {
if (preventClose) {
return;
}
if (!newValue) {
setNewOrgSlug(undefined);
}
form.reset();
setOpen(newValue);
};
if (projectLoading || orgsLoading) {
return <LoadingScreen />;
}
return (
<>
<Dialog open={open} onOpenChange={handleTransferProjectDialogOpenChange}>
<DialogContent className="z-[9999] text-foreground sm:max-w-xl">
<DialogHeader className="flex gap-2">
<DialogTitle>
Move the current project to a different organization.{' '}
</DialogTitle>
{!finishOrgCreation && (
<DialogDescription>
To transfer a project between organizations, you must be an{' '}
<span className="font-bold">ADMIN</span> in both.
<br />
When transferred to a new organization, the project will adopt
that organizations plan.
</DialogDescription>
)}
</DialogHeader>
{finishOrgCreation ? (
<FinishOrgCreation
onCompleted={handleFinishOrgCreationCompleted}
onError={() => setPreventClose(false)}
/>
) : (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
>
<FormField
control={form.control}
name="organization"
render={({ field }) => (
<FormItem>
<FormLabel>Organization</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Organization" />
</SelectTrigger>
</FormControl>
<SelectContent>
{orgs.map((org) => (
<SelectItem
key={org.id}
value={org.id}
disabled={
org.plan.isFree || // disable the personal org
org.id === currentOrg.id || // disable the current org as it can't be a destination org
!isUserAdminOfOrg(org, currentUserId) // disable orgs that the current user is not admin of
}
>
{org.name}
<Badge
variant={
org.plan.isFree ? 'outline' : 'default'
}
className={cn(
org.plan.isFree ? 'bg-muted' : '',
'hover:none ml-2 h-5 px-[6px] text-[10px]',
)}
>
{org.plan.name}
</Badge>
</SelectItem>
))}
<SelectItem
key={CREATE_NEW_ORG}
value={CREATE_NEW_ORG}
>
<div className="flex items-center justify-center gap-2">
<Plus
className="h-4 w-4 font-bold"
strokeWidth={3}
/>{' '}
<span>New Organization</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2">
<Button
variant="secondary"
type="button"
disabled={form.formState.isSubmitting || preventClose}
onClick={() => {
form.reset();
setOpen(false);
}}
>
Cancel
</Button>
<Button
type="submit"
disabled={
form.formState.isSubmitting || !form.formState.isDirty
}
>
{form.formState.isSubmitting ? (
<ActivityIndicator />
) : (
submitButtonText
)}
</Button>
</div>
</form>
</Form>
)}
</DialogContent>
</Dialog>
<CreateOrgDialog
hideNewOrgButton
isOpen={showCreateOrgModal}
onOpenStateChange={handleCreateDialogOpenStateChange}
redirectUrl={redirectUrl}
/>
</>
);
}

View File

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

View File

@@ -130,6 +130,7 @@ export default function DeleteOrg() {
e.preventDefault();
await handleDeleteOrg();
}}
data-testid="deleteOrgButton"
className={buttonVariants({ variant: 'destructive' })}
disabled={
deleting ||

View File

@@ -13,13 +13,13 @@ Object.defineProperty(window, 'matchMedia', {
value: vi.fn().mockImplementation(mockMatchMediaValue),
});
export const getUseRouterObject = (session_id?: string) => ({
export const getUseRouterObject = (session_id?: string, isReady = true) => ({
basePath: '',
pathname: '/orgs/xyz/projects/test-project',
route: '/orgs/[orgSlug]/projects/[appSubdomain]',
asPath: '/orgs/xyz/projects/test-project',
isLocaleDomain: false,
isReady: true,
isReady,
isPreview: false,
query: {
orgSlug: 'xyz',
@@ -113,16 +113,18 @@ const fetchOrganizationNewRequestsResponseMock = async () => ({
const fetchPostOrganizationResponseMock = vi.fn();
test('if there is NO session_id in the url the billingPostOrganizationRequest is fetched from the server', async () => {
test('if there is NO session_id in the url and the router is ready the billingPostOrganizationRequest is fetched from the server', async () => {
server.use(getOrganizations);
mocks.useOrganizationMemberInvitesLazyQuery.mockImplementation(
fetchOrganizationMemberInvitesMock,
);
mocks.useRouter.mockImplementation(() => getUseRouterObject());
mocks.useRouter.mockImplementation(() => getUseRouterObject(undefined, true));
mocks.userData.mockImplementation(() => mockSession.user);
mocks.useOrganizationNewRequestsLazyQuery.mockImplementation(() => [
fetchOrganizationNewRequestsResponseMock,
]);
mocks.usePostOrganizationRequestMutation.mockImplementation(() => [
fetchPostOrganizationResponseMock.mockImplementation(() => ({
data: {
@@ -143,6 +145,39 @@ test('if there is NO session_id in the url the billingPostOrganizationRequest is
expect(fetchPostOrganizationResponseMock).toHaveBeenCalled();
});
test('if the router is not ready the billingPostOrganizationRequest is not fetched from the server', async () => {
server.use(getOrganizations);
mocks.useOrganizationMemberInvitesLazyQuery.mockImplementation(
fetchOrganizationMemberInvitesMock,
);
mocks.useRouter.mockImplementation(() =>
getUseRouterObject(undefined, false),
);
mocks.userData.mockImplementation(() => mockSession.user);
mocks.useOrganizationNewRequestsLazyQuery.mockImplementation(() => [
fetchOrganizationNewRequestsResponseMock,
]);
mocks.usePostOrganizationRequestMutation.mockImplementation(() => [
fetchPostOrganizationResponseMock.mockImplementation(() => ({
data: {
billingPostOrganizationRequest: {
Status: CheckoutStatus.Open,
Slug: 'newOrgSlug',
ClientSecret: 'very_secret_secret',
__typename: 'PostOrganizationRequestResponse',
},
},
})),
]);
render(<NotificationsTray />);
await waitFor(() => {
/* Wait for the component to be update */
});
expect(fetchPostOrganizationResponseMock).not.toHaveBeenCalled();
});
test('if there is a session_id in the url the billingPostOrganizationRequest is NOT fetched from the server ', async () => {
server.use(getOrganizations);
mocks.useOrganizationMemberInvitesLazyQuery.mockImplementation(

View File

@@ -39,7 +39,7 @@ type Invite = OrganizationMemberInvitesQuery['organizationMemberInvites'][0];
export default function NotificationsTray() {
const userData = useUserData();
const { asPath, route, push, query } = useRouter();
const { asPath, route, push, query, isReady: isRouterReady } = useRouter();
const { session_id } = query;
const { refetch: refetchOrgs } = useOrgs();
const [open, setOpen] = useState(false);
@@ -113,6 +113,7 @@ export default function NotificationsTray() {
if (
userData &&
!['/', '/orgs/verify'].includes(route) &&
isRouterReady &&
isEmptyValue(session_id)
) {
checkForPendingOrgRequests();
@@ -120,6 +121,7 @@ export default function NotificationsTray() {
}, [
route,
userData,
isRouterReady,
getOrganizationNewRequests,
postOrganizationRequest,
session_id,

View File

@@ -31,6 +31,7 @@ import { OrgInvite } from '@/features/orgs/components/members/components/OrgInvi
import { useIsOrgAdmin } from '@/features/orgs/hooks/useIsOrgAdmin';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import execPromiseWithErrorToast from '@/features/orgs/utils/execPromiseWithErrorToast/execPromiseWithErrorToast';
import { analytics } from '@/lib/segment';
import {
Organization_Members_Role_Enum,
useGetOrganizationInvitesQuery,
@@ -91,6 +92,16 @@ export default function PendingInvites() {
},
});
analytics.track('Organization Invite Sent', {
organizationId: org?.id,
organizationName: org?.name,
organizationSlug: org?.slug,
organizationPlan: org?.plan?.name,
organizationPlanId: org?.plan?.id,
inviteeEmail: email,
inviteeRole: role,
});
setInviteDialogOpen(false);
setOrgInviteError(null);
form.reset();

View File

@@ -32,9 +32,8 @@ function useFinishOrgCreation({
useEffect(() => {
async function finishOrgCreation() {
if (session_id && isAuthenticated) {
if (router.isReady && session_id && isAuthenticated) {
setLoading(true);
execPromiseWithErrorToast(
async () => {
const {

View File

@@ -1,7 +1,7 @@
import { Container } from '@/components/layout/Container';
import { Modal } from '@/components/ui/v1/Modal';
import { Button } from '@/components/ui/v2/Button';
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
import { TransferOrUpgradeProjectDialog } from '@/features/orgs/components/common/TransferOrUpgradeProjectDialog';
import { ApplicationInfo } from '@/features/orgs/projects/common/components/ApplicationInfo';
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
import { RemoveApplicationModal } from '@/features/orgs/projects/common/components/RemoveApplicationModal';
@@ -51,7 +51,7 @@ export default function ApplicationPaused() {
Transfer
</Button>
<TransferProjectDialog
<TransferOrUpgradeProjectDialog
open={transferProjectDialogOpen}
setOpen={setTransferProjectDialogOpen}
/>

View File

@@ -1,3 +1,4 @@
import { SquareArrowUpRightIcon } from 'lucide-react';
import Link from 'next/link';
import type { PropsWithChildren } from 'react';
@@ -5,7 +6,8 @@ function TextLink({
href,
children,
target = '_blank',
}: PropsWithChildren<{ href: string; target?: string }>) {
withIcon = false,
}: PropsWithChildren<{ href: string; target?: string; withIcon?: boolean }>) {
return (
<Link
href={href}
@@ -14,6 +16,7 @@ function TextLink({
rel="noopener noreferrer"
>
{children}
{withIcon && <SquareArrowUpRightIcon className="h-4 w-4" />}
</Link>
);
}

View File

@@ -23,7 +23,7 @@ export default function useGetPostgresVersion() {
});
const { version } = postgresSettingsData?.config?.postgres || {};
const { major, minor } = splitPostgresMajorMinorVersions(version);
const { major, minor } = splitPostgresMajorMinorVersions(version || '');
return {
version,

View File

@@ -21,6 +21,7 @@ import {
useGetPostgresSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { isEmptyValue } from '@/lib/utils';
import { ApplicationStatus } from '@/types/application';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useMemo } from 'react';
@@ -46,7 +47,9 @@ export default function DatabaseStorageCapacity() {
const localMimirClient = useLocalMimirClient();
const { project } = useProject();
const isFreeProject = !!org?.plan?.isFree;
const isFreeProject = isEmptyValue(org) ? false : org.plan.isFree;
const shouldShowUpdateCapacityWarning = !isFreeProject && isPlatform;
const {
data,
@@ -98,6 +101,10 @@ export default function DatabaseStorageCapacity() {
return true;
}
if (!isPlatform) {
return false;
}
if (maintenanceActive) {
return true;
}
@@ -107,7 +114,13 @@ export default function DatabaseStorageCapacity() {
}
return false;
}, [isDirty, maintenanceActive, decreasingSize, applicationPause]);
}, [
isDirty,
maintenanceActive,
decreasingSize,
applicationPause,
isPlatform,
]);
useEffect(() => {
if (data && !loading) {
@@ -195,7 +208,7 @@ export default function DatabaseStorageCapacity() {
helperText={formState.errors.capacity?.message}
/>
</Box>
{!isFreeProject && (
{shouldShowUpdateCapacityWarning && (
<DatabaseStorageCapacityWarning
state={state}
decreasingSize={decreasingSize}

View File

@@ -4,7 +4,7 @@ import { Alert } from '@/components/ui/v2/Alert';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
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';
interface Props {
@@ -54,7 +54,7 @@ function UpgradeNotification({ description }: Props) {
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
<OpenTransferDialogButton onClick={handleTransferDialogOpen} />
<TransferProjectDialog
<TransferOrUpgradeProjectDialog
open={transferProjectDialogOpen}
setOpen={setTransferProjectDialogOpen}
/>

View File

@@ -7,6 +7,7 @@ import { EditRepositoryAndBranchSettings } from '@/features/orgs/projects/git/co
import type { EditRepositorySettingsFormData } from '@/features/orgs/projects/git/common/components/EditRepositorySettings';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { useUpdateApplicationMutation } from '@/generated/graphql';
import { analytics } from '@/lib/segment';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import { useFormContext } from 'react-hook-form';
@@ -49,6 +50,17 @@ export default function EditRepositorySettingsModal({
},
},
});
if (selectedRepoId) {
analytics.track('Project Connected to GitHub', {
projectId: project.id,
projectName: project.name,
projectSubdomain: project.subdomain,
repositoryId: selectedRepoId,
productionBranch: data.productionBranch,
baseFolder: data.repoBaseFolder,
});
}
} else {
await updateApp({
variables: {

View File

@@ -0,0 +1,67 @@
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { UserSelect } from '@/features/orgs/projects/graphql/common/components/UserSelect';
import { useState } from 'react';
/**
* Component that combines user selection and role selection functionality
*/
interface UserAndRoleSelectProps {
/**
* Function to be called when the user changes.
*/
onUserChange: (userId: string) => void;
/**
* Function to be called when the role changes.
*/
onRoleChange: (role: string) => void;
}
export default function UserAndRoleSelect({
onUserChange,
onRoleChange,
}: UserAndRoleSelectProps) {
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
const [role, setRole] = useState<string>(() => availableRoles[0]);
const handleUserChange = (userId: string, availableUserRoles: string[]) => {
onUserChange(userId);
setAvailableRoles(availableUserRoles);
const newRole = availableUserRoles[0];
if (newRole) {
setRole(newRole);
onRoleChange(newRole);
}
};
return (
<div className="grid grid-cols-2 gap-2 md:grid-flow-col md:grid-cols-[initial]">
<UserSelect
className="col-span-1 md:col-auto md:w-52"
onUserChange={handleUserChange}
/>
<Select
id="role-select"
label="Role"
value={role}
onChange={(_event, value) => {
if (typeof value === 'string') {
setRole(value);
onRoleChange(value);
}
}}
hideEmptyHelperText
className="col-span-1 md:col-auto md:w-52"
>
{availableRoles.map((availableRole) => (
<Option value={availableRole} key={availableRole}>
{availableRole}
</Option>
))}
</Select>
</div>
);
}

View File

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

View File

@@ -1,9 +1,10 @@
import { Autocomplete } from '@/components/ui/v2/Autocomplete';
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
import { DEFAULT_ROLES } from '@/features/orgs/projects/graphql/common/utils/constants';
import { getAdminRoles } from '@/features/orgs/projects/roles/settings/utils/getAdminRoles';
import { isNotEmptyValue } from '@/lib/utils';
import {
useRemoteAppGetUsersCustomLazyQuery,
type RemoteAppGetUsersCustomQuery,
useRemoteAppGetUsersAndAuthRolesLazyQuery,
type RemoteAppGetUsersAndAuthRolesQuery,
} from '@/utils/__generated__/graphql';
import { debounce } from '@mui/material/utils';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -26,22 +27,26 @@ export default function UserSelect({
const [inputValue, setInputValue] = useState('');
const [users, setUsers] = useState([]);
const [active, setActive] = useState(true);
const [adminAuthRoles, setAdminAuthRoles] = useState<string[]>(() =>
getAdminRoles(),
); // Roles from the auth.roles table
const userApplicationClient = useRemoteApplicationGQLClient();
const [fetchAppUsers, { loading }] = useRemoteAppGetUsersCustomLazyQuery({
client: userApplicationClient,
variables: {
where: {},
limit: 250,
offset: 0,
},
});
const [fetchAppUsers, { loading }] =
useRemoteAppGetUsersAndAuthRolesLazyQuery({
client: userApplicationClient,
variables: {
where: {},
limit: 250,
offset: 0,
},
});
const fetchUsers = useCallback(
async (
request: { input: string },
callback: (results?: RemoteAppGetUsersCustomQuery['users']) => void,
callback: (results?: RemoteAppGetUsersAndAuthRolesQuery) => void,
) => {
const ilike = `%${request.input === 'Admin' ? '' : request.input}%`;
const { data } = await fetchAppUsers({
@@ -55,7 +60,7 @@ export default function UserSelect({
},
});
callback(data?.users);
callback(data);
},
[fetchAppUsers, userApplicationClient],
);
@@ -65,11 +70,28 @@ export default function UserSelect({
useEffect(() => {
fetchOptions({ input: inputValue }, (results) => {
if (active || inputValue === '') {
setUsers(results || []);
if (
isNotEmptyValue(results?.users) &&
isNotEmptyValue(results?.authRoles)
) {
setUsers(results.users);
const newAuthRoles = results.authRoles.map(
(authRole) => authRole.role,
);
setAdminAuthRoles(newAuthRoles);
} else {
setUsers([]);
setAdminAuthRoles(getAdminRoles());
}
}
});
}, [inputValue, fetchOptions, active]);
useEffect(() => {
onUserChange('admin', getAdminRoles(adminAuthRoles));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [adminAuthRoles]);
const autocompleteOptions = [
{
value: 'admin',
@@ -109,24 +131,19 @@ export default function UserSelect({
}
if (userId === 'admin') {
onUserChange('admin', DEFAULT_ROLES);
onUserChange('admin', getAdminRoles(adminAuthRoles));
return;
}
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
const user: RemoteAppGetUsersAndAuthRolesQuery['users'][0] = users.find(
({ id }) => id === userId,
);
const roles = user?.roles?.map(({ role }) => role);
if (isNotEmptyValue(user?.roles)) {
const roles = user.roles.map(({ role }) => role);
onUserChange(userId, roles ?? DEFAULT_ROLES);
fetchUsers({ input: '' }, (results) => {
if (results) {
setUsers(results);
}
});
onUserChange(userId, roles);
}
}}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);

View File

@@ -1,3 +0,0 @@
const DEFAULT_ROLES = ['admin', 'public', 'anonymous'];
export { DEFAULT_ROLES };

View File

@@ -1 +0,0 @@
export * from './constants';

View File

@@ -6,11 +6,13 @@ import { Dropdown } from '@/components/ui/v2/Dropdown';
import { CalendarIcon } from '@/components/ui/v2/icons/CalendarIcon';
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { Text } from '@/components/ui/v2/Text';
import type { LogsFilterFormValues } from '@/features/orgs/projects/logs/components/LogsHeader';
import { LogsTimePicker } from '@/features/orgs/projects/logs/components/LogsTimePicker';
import { DATEPICKER_DISPLAY_FORMAT } from '@/features/orgs/projects/logs/utils/constants/datePicker';
import { usePreviousData } from '@/hooks/usePreviousData';
import { format } from 'date-fns';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface LogsDatePickerProps extends DatePickerProps {
@@ -36,6 +38,7 @@ function LogsDatePicker({
value,
}: LogsDatePickerProps) {
const [selectedDate, setSelectedDate] = useState<Date | null>(value);
const { setValue } = useFormContext<LogsFilterFormValues>();
const { button: buttonSlotProps } = {
button: componentsProps?.button || {},
};
@@ -45,6 +48,11 @@ function LogsDatePicker({
// going to display the last state set.
const previousDate = usePreviousData(selectedDate);
const handleDateChange = (newValue: Date) => {
setSelectedDate(new Date(newValue));
setValue('interval', null);
};
return (
<Dropdown.Root>
<div className="grid grid-flow-col gap-x-3">
@@ -84,9 +92,7 @@ function LogsDatePicker({
<Dropdown.Content>
<DatePicker
value={disabled ? previousDate : selectedDate}
onChange={(newValue) => {
setSelectedDate(new Date(newValue));
}}
onChange={handleDateChange}
minDate={minDate}
maxDate={maxDate}
/>

View File

@@ -17,7 +17,7 @@ import {
} from '@/features/orgs/projects/logs/utils/constants/services';
import { isEmptyValue } from '@/lib/utils';
import { useGetServiceLabelValuesQuery } from '@/utils/__generated__/graphql';
import { MINUTES_TO_DECREASE_FROM_CURRENT_DATE } from '@/utils/constants/common';
import { DEFAULT_LOG_INTERVAL } from '@/utils/constants/common';
import { yupResolver } from '@hookform/resolvers/yup';
import { subMinutes } from 'date-fns';
import { useEffect, useMemo } from 'react';
@@ -28,6 +28,7 @@ import LogsServiceFilter from './LogsServiceFilter';
export const validationSchema = Yup.object({
from: Yup.date(),
to: Yup.date().nullable(),
interval: Yup.number().nullable(), // in minutes
service: Yup.string().oneOf(Object.values(AvailableLogsService)),
regexFilter: Yup.string(),
});
@@ -44,11 +45,17 @@ interface LogsHeaderProps extends Omit<BoxProps, 'children'> {
* Function to be called when the user submits the filters form
*/
onSubmitFilterValues: (value: LogsFilterFormValues) => void;
/**
*
* Function to be called to force a refetch of the logs when the form is not dirty and the user submits the form
*/
onRefetch: () => void;
}
export default function LogsHeader({
loading,
onSubmitFilterValues,
onRefetch,
...props
}: LogsHeaderProps) {
const { project } = useProject();
@@ -83,16 +90,20 @@ export default function LogsHeader({
const form = useForm<LogsFilterFormValues>({
defaultValues: {
from: subMinutes(new Date(), MINUTES_TO_DECREASE_FROM_CURRENT_DATE),
from: subMinutes(new Date(), DEFAULT_LOG_INTERVAL),
to: new Date(),
regexFilter: '',
service: AvailableLogsService.ALL,
interval: DEFAULT_LOG_INTERVAL,
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
reValidateMode: 'onSubmit',
});
const { formState } = form;
const { register, watch, getValues } = form;
const isNotDirty = Object.keys(formState.dirtyFields).length === 0;
const { register, watch, getValues, setValue } = form;
const service = watch('service');
@@ -100,8 +111,33 @@ export default function LogsHeader({
onSubmitFilterValues(getValues());
}, [service, getValues, onSubmitFilterValues]);
const handleSubmit = (values: LogsFilterFormValues) =>
const handleSubmit = (values: LogsFilterFormValues) => {
// If there's an interval set, recalculate the dates
if (values.interval) {
const now = new Date();
const newValues = {
...values,
from: subMinutes(now, values.interval),
to: now,
interval: values.interval,
};
// Update form values before submitting, to ensure the dates have the current date if selected an interval
setValue('from', newValues.from);
setValue('to', newValues.to);
setValue('interval', newValues.interval);
onSubmitFilterValues(newValues);
return;
}
// If the form is not dirty, force a refetch of the logs
if (isNotDirty) {
onRefetch();
}
onSubmitFilterValues(values);
};
return (
<Box
@@ -198,11 +234,13 @@ export default function LogsHeader({
type="submit"
className="h-10"
startIcon={
loading ? (
<ActivityIndicator className="h-4 w-4" />
) : (
<SearchIcon />
)
<div className="flex h-5 w-5 items-center justify-center">
{loading ? (
<ActivityIndicator className="h-5 w-5" />
) : (
<SearchIcon className="h-5 w-5" />
)}
</div>
}
disabled={loading}
>

View File

@@ -27,11 +27,18 @@ function LogsToDatePickerLiveButton() {
if (isLive) {
setValue('from', subMinutes(new Date(), 20));
setValue('to', new Date());
setValue('interval', null);
return;
}
setValue('to', null);
setCurrentTime(new Date());
setValue('interval', null);
}
function handleChangeToDate(date: Date) {
setValue('to', date);
setValue('interval', null);
}
useInterval(() => setCurrentTime(new Date()), isLive ? 1000 : 0);
@@ -43,7 +50,7 @@ function LogsToDatePickerLiveButton() {
label="To"
value={!isLive ? to : currentTime}
disabled={isLive}
onChange={(date: Date) => setValue('to', date)}
onChange={handleChangeToDate}
minDate={from}
maxDate={new Date()}
componentsProps={{
@@ -84,7 +91,7 @@ function LogsRangeSelectorIntervalPickers({
const applicationCreationDate = new Date(project.createdAt);
const { setValue, getValues } = useFormContext<LogsFilterFormValues>();
const { from } = useWatch<LogsFilterFormValues>();
const { from, interval } = useWatch<LogsFilterFormValues>();
const { handleClose } = useDropdown();
@@ -101,6 +108,12 @@ function LogsRangeSelectorIntervalPickers({
}: LogsCustomInterval) {
setValue('from', subMinutes(new Date(), minutesToDecreaseFromCurrentDate));
setValue('to', new Date());
setValue('interval', minutesToDecreaseFromCurrentDate);
}
function handleChangeFromDate(date: Date) {
setValue('from', date);
setValue('interval', null);
}
return (
@@ -109,7 +122,7 @@ function LogsRangeSelectorIntervalPickers({
<LogsDatePicker
label="From"
value={from}
onChange={(date) => setValue('from', date)}
onChange={handleChangeFromDate}
minDate={applicationCreationDate}
maxDate={new Date()}
/>
@@ -122,7 +135,11 @@ function LogsRangeSelectorIntervalPickers({
<Button
key={logInterval.label}
variant="outlined"
color="secondary"
color={
interval === logInterval.minutesToDecreaseFromCurrentDate
? 'primary'
: 'secondary'
}
className="self-center"
onClick={() => handleIntervalChange(logInterval)}
>

View File

@@ -5,10 +5,10 @@ import { Button } from '@/components/ui/v3/button';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import UpgradeProjectDialog from '@/features/orgs/projects/overview/components/OverviewTopBar/UpgradeProjectDialog';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import Image from 'next/image';
import Link from 'next/link';
import UpgradeProjectDialog from './UpgradeProjectDialog';
export default function OverviewTopBar() {
const isPlatform = useIsPlatform();

View File

@@ -1,5 +1,5 @@
import { OpenTransferDialogButton } from '@/components/common/OpenTransferDialogButton';
import { TransferProjectDialog } from '@/features/orgs/components/common/TransferProjectDialog';
import { TransferOrUpgradeProjectDialog } from '@/features/orgs/components/common/TransferOrUpgradeProjectDialog';
import { useCallback, useState } from 'react';
function UpgradeProjectDialog() {
@@ -12,7 +12,7 @@ function UpgradeProjectDialog() {
buttonText="Upgrade project"
onClick={handleDialogOpen}
/>
<TransferProjectDialog open={open} setOpen={setOpen} />
<TransferOrUpgradeProjectDialog open={open} setOpen={setOpen} isUpgrade />
</>
);
}

View File

@@ -0,0 +1,29 @@
import { expect, test } from 'vitest';
import getAdminRoles from './getAdminRoles';
test('should return an array with the default admin roles if no roles are passed', () => {
expect(getAdminRoles()).toEqual(['admin', 'public', 'anonymous']);
expect(getAdminRoles(null)).toEqual(['admin', 'public', 'anonymous']);
expect(getAdminRoles(undefined)).toEqual(['admin', 'public', 'anonymous']);
});
test('should return an array with the admin roles and the given roles', () => {
expect(getAdminRoles(['anonymous', 'me', 'user', 'test_user'])).toEqual([
'admin',
'public',
'anonymous',
'me',
'user',
'test_user',
]);
});
test('should return an array with the admin roles and the given roles without duplicates', () => {
expect(getAdminRoles(['anonymous', 'me', 'user', 'admin'])).toEqual([
'admin',
'public',
'anonymous',
'me',
'user',
]);
});

View File

@@ -0,0 +1,17 @@
import { isEmptyValue } from '@/lib/utils';
/**
* Adds the admin public and anonymous roles to the given list of roles with duplicates
*
* @param roles - Roles from auth.roles table in string format
* @returns An array with the admin roles
*/
export default function getAdminRoles(roles?: string[]) {
if (isEmptyValue(roles)) {
return ['admin', 'public', 'anonymous'];
}
const rolesSet = new Set(['admin', 'public', 'anonymous', ...roles]);
return Array.from(rolesSet);
}

View File

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

View File

@@ -2,13 +2,10 @@ import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Button } from '@/components/ui/v2/Button';
import { PlayIcon } from '@/components/ui/v2/icons/PlayIcon';
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
import { UserSelect } from '@/features/orgs/projects/graphql/common/components/UserSelect';
import { DEFAULT_ROLES } from '@/features/orgs/projects/graphql/common/utils/constants';
import { UserAndRoleSelect } from '@/features/orgs/projects/graphql/common/components/UserAndRoleSelect';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { triggerToast } from '@/utils/toast';
import {
@@ -41,11 +38,6 @@ interface GraphiQLHeaderProps {
}
function GraphiQLHeader({ onUserChange, onRoleChange }: GraphiQLHeaderProps) {
const [availableRoles, setAvailableRoles] = useState<string[]>(DEFAULT_ROLES);
const [role, setRole] = useState<string>(() =>
availableRoles.includes('user') ? 'user' : availableRoles[0],
);
const copyQuery = useCopyQuery();
const prettifyEditors = usePrettifyEditors();
const {
@@ -119,42 +111,10 @@ function GraphiQLHeader({ onUserChange, onRoleChange }: GraphiQLHeaderProps) {
return (
<header className="grid grid-flow-row items-end gap-2 p-2 md:grid-flow-col md:justify-between">
<div className="grid grid-flow-row gap-2 md:grid-flow-col md:items-end">
<div className="grid grid-cols-2 gap-2 md:grid-flow-col md:grid-cols-[initial]">
<UserSelect
className="col-span-1 md:col-auto md:w-52"
onUserChange={(userId, availableUserRoles) => {
onUserChange(userId);
setAvailableRoles(availableUserRoles);
const newRole = availableUserRoles.includes('user')
? 'user'
: availableUserRoles[0];
setRole(newRole);
onRoleChange(newRole);
}}
/>
<Select
id="role-select"
label="Role"
value={role}
onChange={(_event, value) => {
if (typeof value === 'string') {
setRole(value);
onRoleChange(value);
}
}}
hideEmptyHelperText
className="col-span-1 md:col-auto md:w-52"
>
{availableRoles.map((availableRole) => (
<Option value={availableRole} key={availableRole}>
{availableRole}
</Option>
))}
</Select>
</div>
<UserAndRoleSelect
onUserChange={onUserChange}
onRoleChange={onRoleChange}
/>
<div className="grid grid-cols-2 gap-2 md:grid-flow-col md:grid-cols-[initial]">
<Tooltip title="Prettify query (Shift+Ctrl+P)">

View File

@@ -12,7 +12,7 @@ import {
GetLogsSubscriptionDocument,
useGetProjectLogsQuery,
} from '@/utils/__generated__/graphql';
import { MINUTES_TO_DECREASE_FROM_CURRENT_DATE } from '@/utils/constants/common';
import { DEFAULT_LOG_INTERVAL } from '@/utils/constants/common';
import { subMinutes } from 'date-fns';
import {
useCallback,
@@ -37,7 +37,7 @@ export default function LogsPage() {
const subscriptionReturn = useRef(null);
const [filters, setFilters] = useState<LogsFilters>({
from: subMinutes(new Date(), MINUTES_TO_DECREASE_FROM_CURRENT_DATE),
from: subMinutes(new Date(), DEFAULT_LOG_INTERVAL),
to: new Date(),
regexFilter: '',
service: AvailableLogsService.ALL,
@@ -48,6 +48,7 @@ export default function LogsPage() {
error,
subscribeToMore,
client,
refetch,
loading: loadingLogs,
} = useGetProjectLogsQuery({
variables: { appID: project?.id, ...filters },
@@ -147,6 +148,7 @@ export default function LogsPage() {
<LogsHeader
loading={loading}
onSubmitFilterValues={onSubmitFilterValues}
onRefetch={refetch}
/>
<LogsBody error={error} loading={loading} logsData={data} />
</RetryableErrorBoundary>

View File

@@ -78,7 +78,10 @@ export default function SettingsGeneralPage() {
usePauseApplicationMutation({
variables: { appId: project?.id },
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});
@@ -86,7 +89,10 @@ export default function SettingsGeneralPage() {
useUnpauseApplicationMutation({
variables: { appId: project?.id },
refetchQueries: [
{ query: GetOrganizationsDocument, variables: { userId: userData.id } },
{
query: GetOrganizationsDocument,
variables: { userId: userData?.id },
},
],
});

View File

@@ -12,6 +12,7 @@ import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import { useSubmitState } from '@/hooks/useSubmitState';
import { analytics } from '@/lib/segment';
import {
useInsertOrgApplicationMutation,
usePrefetchNewAppQuery,
@@ -113,6 +114,15 @@ export function NewProjectPageContent({
});
if (subdomain) {
analytics.track('Project Created', {
projectName: name,
projectSlug: slug,
organizationId: selectedOrg.id,
organizationName: selectedOrg.name,
regionId: selectedRegion.id,
regionName: selectedRegion.name,
});
await router.push(`/orgs/${selectedOrg.slug}/projects/${subdomain}`);
}
},

View File

@@ -3,10 +3,11 @@
import { BaseLayout } from '@/components/layout/BaseLayout';
import { Header } from '@/components/layout/Header';
import { FinishOrgCreationProcess } from '@/features/orgs/components/common/FinishOrgCreationProcess';
import { useFinishOrgCreation } from '@/features/orgs/hooks/useFinishOrgCreation';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { analytics } from '@/lib/segment';
import type { PostOrganizationRequestMutation } from '@/utils/__generated__/graphql';
import { useAuthenticationStatus } from '@nhost/nextjs';
import { useGetOrganizationLazyQuery } from '@/utils/__generated__/graphql';
import { useAuthenticationStatus, useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { useCallback, useEffect } from 'react';
@@ -14,6 +15,8 @@ export default function PostCheckout() {
const router = useRouter();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading } = useAuthenticationStatus();
const currentUser = useUserData();
const [getOrganizations] = useGetOrganizationLazyQuery();
useEffect(() => {
if (!isPlatform || isLoading || isAuthenticated) {
@@ -24,24 +27,38 @@ export default function PostCheckout() {
}, [isLoading, isAuthenticated, router, isPlatform]);
const onCompleted = useCallback(
(
async (
data: PostOrganizationRequestMutation['billingPostOrganizationRequest'],
) => {
const { Slug } = data;
const { data: orgData } = await getOrganizations({
variables: {
orgSlug: Slug,
},
});
const { id, name, slug, plan } = orgData.organizations[0];
analytics.track('Organization Created', {
organizationId: id,
organizationSlug: slug,
organizationName: name,
organizationPlan: plan?.name,
organizationOwnerId: currentUser?.id,
organizationOwnerEmail: currentUser?.email,
});
router.push(`/orgs/${Slug}/projects`);
},
[router],
[router, currentUser?.email, currentUser?.id, getOrganizations],
);
const [loading, status] = useFinishOrgCreation({ onCompleted });
return (
<BaseLayout className="flex h-screen flex-col">
<Header className="flex py-1" />
<div className="flex h-screen w-full flex-col">
<FinishOrgCreationProcess
loading={loading}
status={status}
onCompleted={onCompleted}
loadingMessage="Processing new organization request"
successMessage="Organization created successfully. Redirecting..."
pendingMessage="Organization creation is pending..."

View File

@@ -7,6 +7,7 @@ import { Divider } from '@/components/ui/v2/Divider';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { analytics } from '@/lib/segment';
import { getToastStyleProps } from '@/utils/constants/settings';
import { nhost } from '@/utils/nhost';
import { yupResolver } from '@hookform/resolvers/yup';
@@ -39,6 +40,19 @@ export default function SignUpPage() {
const { signUpEmailPassword, error } = useSignUpEmailPassword();
const [loading, setLoading] = useState(false);
const router = useRouter();
const [anonId, setAnonId] = useState<string | null>(null);
useEffect(() => {
const getAnonId = async () => {
try {
const user = await analytics.user();
setAnonId(user.anonymousId());
} catch (err) {
console.error('Failed to get anonymous ID:', err);
}
};
getAnonId();
}, []);
// x-cf-turnstile-response
const [turnstileResponse, setTurnstileResponse] = useState(null);
@@ -85,6 +99,7 @@ export default function SignUpPage() {
password,
{
displayName,
metadata: { anonId },
},
{
headers: {
@@ -126,7 +141,10 @@ export default function SignUpPage() {
setLoading(true);
try {
await nhost.auth.signIn({ provider: 'github' });
await nhost.auth.signIn({
provider: 'github',
options: { metadata: { anonId } },
});
} catch {
toast.error(
`An error occurred while trying to sign up using GitHub. Please try again.`,

View File

@@ -67,4 +67,4 @@ export const MAX_FREE_PROJECTS = 1;
/**
* Default value in minutes to use for querying the logs
*/
export const MINUTES_TO_DECREASE_FROM_CURRENT_DATE = 20;
export const DEFAULT_LOG_INTERVAL = 15;

View File

@@ -1,5 +1,15 @@
# @nhost/docs
## 2.31.0
### Minor Changes
- b302dbd: feat: added sveltekit quickstart
### Patch Changes
- 5e96230: fix: fixing mintlify breaking our docs
## 2.30.0
### Minor Changes

View File

@@ -2,6 +2,11 @@
"$schema": "https://mintlify.com/docs.json",
"theme": "mint",
"name": "Documentation",
"integrations": {
"segment": {
"key": "kD6QfDOMGR2IoJ9D1U1H5Q9X7AEjoVfN"
}
},
"colors": {
"primary": "#3787ff",
"light": "#569aff",
@@ -31,6 +36,7 @@
"/getting-started/quickstart/react",
"/getting-started/quickstart/nextjs",
"/getting-started/quickstart/vue",
"/getting-started/quickstart/sveltekit",
"/getting-started/quickstart/reactnative"
]
},

View File

@@ -19,7 +19,7 @@ It eliminates backend complexity by providing a performant and reliable software
src="/images/nhost-overview-dark.svg"
alt="Hero Dark"
/>
## Next steps
@@ -47,9 +47,16 @@ Follow one of our quick start guides for learning how to quickly setup Nhost wit
>
Learn how to connect Nhost with Vue
</Card>
<Card
title="SvelteKit"
icon="s"
href="/getting-started/tutorials/sveltekit"
>
Learn how to connect Nhost with SvelteKit
</Card>
<Card
title="React Native"
icon="react-native"
icon="mobile-notch"
href="/getting-started/quickstart/reactnative"
>
Learn how to connect Nhost with React Native
@@ -62,6 +69,13 @@ Follow one of our quick start guides for learning how to quickly setup Nhost wit
Follow one of your tutorials where we walk you through building a Todo Manager application using features from Nhost.
<CardGroup cols={2}>
<Card
title="Next.js"
icon="react"
href="/getting-started/tutorials/nextjs"
>
Todo Manager with Nhost and NextJS
</Card>
<Card
title="React"
icon="react"
@@ -76,13 +90,6 @@ Follow one of your tutorials where we walk you through building a Todo Manager a
>
Todo Manager with Nhost and Vue
</Card>
<Card
title="Next.js"
icon="image"
href="/getting-started/tutorials/nextjs"
>
Todo Manager with Nhost and NextJS
</Card>
</CardGroup>

View File

@@ -0,0 +1,133 @@
---
title: Setup Nhost with SvelteKit
sidebarTitle: SvelteKit
description: Get up and running with Nhost and SvelteKit
icon: s
---
<Steps>
<Step title="Create Project">
If you haven't, please create a project through the [Nhost Dashboard](https://app.nhost.io).
</Step>
<Step title="Setup Database">
Navigate to the **SQL Editor** of the database and run the following SQL to create a new table `movies` with some great movies.
```sql SQL Editor
CREATE TABLE movies (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
director VARCHAR(255),
release_year INTEGER,
genre VARCHAR(100),
rating FLOAT
);
INSERT INTO movies (title, director, release_year, genre, rating) VALUES
('Inception', 'Christopher Nolan', 2010, 'Sci-Fi', 8.8),
('The Godfather', 'Francis Ford Coppola', 1972, 'Crime', 9.2),
('Forrest Gump', 'Robert Zemeckis', 1994, 'Drama', 8.8),
('The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999, 'Action', 8.7);
```
<Warning>Make sure the option `Track this` is enabled</Warning>
![SQL Editor](/images/quickstarts/react/sql-editor.png)
</Step>
<Step title="permissions">
Select the new table `movies` just created, and click in **Edit Permissions** to set the following permissions for the `public` role and `select` action.
![Permission Rules](/images/quickstarts/react/permissions.png)
</Step>
<Step title="Setup a SvelteKit Application">
Create a SvelteKit application.
```bash Terminal
mkdir nhost-sveltekit-quickstart && \
cd nhost-sveltekit-quickstart && \
npx sv create --template minimal --no-types --no-add-ons --install npm
```
</Step>
<Step title="Install the Nhost package for SvelteKit">
Navigate to the SvelteKit application and install `@nhost/nhost-js`.
```bash Terminal
npm install @nhost/nhost-js
```
</Step>
<Step title="Configure the Nhost client and fetch the list of movies">
Create a new file with the following code to creates the Nhost client.
```js ./src/lib/nhost.js
import { NhostClient } from "@nhost/nhost-js";
export const nhost = new NhostClient({
subdomain: "<subdomain>",
region: "<region>",
})
```
<Note>Replace `<subdomain>` and `<region>` with the subdomain and region for the project</Note>
Finally, update `src/routes/+page.svelte` to fetch the list of movies.
```js src/routes/+page.svelte
<script>
import { onMount } from 'svelte';
import { nhost } from '../lib/nhost';
let loading = true;
let movies = [];
const getMovies = `
query {
movies {
title
genre
rating
}
}
`;
onMount(async () => {
const { data, error } = await nhost.graphql.request(getMovies);
if (!error) {
movies = data.movies;
}
loading = false;
});
</script>
{#if loading}
<p>Loading...</p>
{:else}
<table>
<tbody>
{#each movies as movie}
<tr>
<td>{movie.title}</td>
<td>{movie.genre}</td>
<td>{movie.rating}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
```
</Step>
<Step title="The end">
Run your project with `npm run dev` and navigate to `http://localhost:5173` in your browser.
</Step>
</Steps>

View File

@@ -1,11 +1,11 @@
{
"name": "@nhost/docs",
"version": "2.30.0",
"version": "2.31.0",
"private": true,
"scripts": {
"start": "mintlify dev"
},
"devDependencies": {
"mintlify": "^4.0.445"
"mintlify": "^4.0.476"
}
}

View File

@@ -5,7 +5,7 @@ mode: "custom"
<div className="welcome-page">
<div className="welcome-hero">
# Platform
<h1>Platform</h1>
<p>Learn about platform features</p>
<b>
[Explore Nhost Cloud →](/platform/cloud)
@@ -13,7 +13,7 @@ mode: "custom"
</div>
<div className="welcome-get-started">
# Explore different deployment options
<h1>Explore different deployment options</h1>
<CardGroup cols={3}>
<Card

View File

@@ -6,7 +6,7 @@ mode: "custom"
<div className="welcome-page">
<div className="welcome-hero">
# Products
<h1>Products</h1>
<p>Turn-key convenience plus extensibility</p>
<b>
[Explore products →](/products/database/overview)
@@ -14,7 +14,7 @@ mode: "custom"
</div>
<div className="welcome-get-started">
# Nhost Stack
<h1>Nhost Stack</h1>
<CardGroup cols={2}>
<Card
@@ -51,7 +51,7 @@ mode: "custom"
</CardGroup>
# Nhost Extend
<h1>Nhost Extend</h1>
<CardGroup cols={3}>
<Card
title="Run"

View File

@@ -5,7 +5,7 @@ mode: "custom"
<div className="welcome-page">
<div className="welcome-hero">
# Build. Deploy. Scale.
<h1>Build. Deploy. Scale.</h1>
<p>Learn how to get started with Nhost</p>
<b>
[Explore the docs →](/getting-started)
@@ -13,7 +13,7 @@ mode: "custom"
</div>
<div className="welcome-get-started">
# Get started with our guides or explore our community
<h1>Get started with our guides or explore our community</h1>
<CardGroup cols={3}>
<Card
@@ -59,7 +59,7 @@ mode: "custom"
Deployable applications built with Nhost
</Card>
</CardGroup>
</div>
</div>

View File

@@ -1,5 +1,11 @@
# @nhost-examples/cli
## 0.3.21
### Patch Changes
- @nhost/nhost-js@3.2.8
## 0.3.20
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/cli",
"version": "0.3.20",
"version": "0.3.21",
"main": "src/index.mjs",
"private": true,
"scripts": {

View File

@@ -1,5 +1,19 @@
# @nhost-examples/codegen-react-apollo
## 0.8.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 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
## 0.7.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-apollo",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"scripts": {
"codegen": "graphql-codegen",
@@ -36,6 +36,6 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.9.5",
"vite": "^5.4.17"
"vite": "^5.4.19"
}
}

View File

@@ -1,5 +1,18 @@
# @nhost-examples/codegen-react-query
## 0.8.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 0.7.1
### Patch Changes
- d9eb906: fix: update vite and nextjs because of vulnerability
- @nhost/react@3.10.4
## 0.7.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/codegen-react-query",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"scripts": {
"codegen": "graphql-codegen",
@@ -37,6 +37,6 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.9.5",
"vite": "^5.4.17"
"vite": "^5.4.19"
}
}

View File

@@ -1,5 +1,19 @@
# @nhost-examples/react-urql
## 0.7.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 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
## 0.6.0
### Minor Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/codegen-react-urql",
"private": true,
"version": "0.6.0",
"version": "0.7.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
@@ -30,6 +30,6 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.9.5",
"vite": "^5.4.17"
"vite": "^5.4.19"
}
}

View File

@@ -1,5 +1,11 @@
# @nhost-examples/multi-tenant-one-to-many
## 2.2.22
### Patch Changes
- @nhost/nhost-js@3.2.8
## 2.2.21
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/multi-tenant-one-to-many",
"private": true,
"version": "2.2.21",
"version": "2.2.22",
"description": "",
"main": "index.js",
"scripts": {},

View File

@@ -1,5 +1,16 @@
# @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
## 0.4.6
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs",
"version": "0.4.6",
"version": "0.4.7",
"private": true,
"scripts": {
"dev": "next dev",
@@ -24,7 +24,7 @@
"@nhost/react": "workspace:^",
"@nhost/react-apollo": "workspace:^",
"graphql": "16.8.1",
"next": "^14.2.25",
"next": "^14.2.26",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.12.0"

View File

@@ -3,7 +3,7 @@ import { NextPage } from 'next'
import { Container, Title } from '@mantine/core'
import { useAccessToken, useAuthenticated } from '@nhost/nextjs'
const PublicSSRPage: NextPage = () => {
const PublicCSRPage: NextPage = () => {
const isAuthenticated = useAuthenticated()
const accessToken = useAccessToken()
return (
@@ -15,4 +15,4 @@ const PublicSSRPage: NextPage = () => {
)
}
export default PublicSSRPage
export default PublicCSRPage

View File

@@ -1,5 +1,11 @@
# @nhost-examples/node-storage
## 0.2.21
### Patch Changes
- @nhost/nhost-js@3.2.8
## 0.2.20
### Patch Changes

View File

@@ -8,7 +8,7 @@ Make sure to install the dependencies:
pnpm install
```
## Settting up the environment
## Setting up the environment
Create a `.env` file in the root of the project with the following content:

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/node-storage",
"version": "0.2.20",
"version": "0.2.21",
"private": true,
"description": "This is an example of how to use the Storage with Node.js",
"main": "src/index.mjs",

View File

@@ -1,5 +1,12 @@
# @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
## 0.5.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/nextjs-server-components",
"version": "0.5.5",
"version": "0.5.6",
"private": true,
"scripts": {
"dev": "next dev",
@@ -18,7 +18,7 @@
"form-data": "^4.0.0",
"graphql": "16.8.1",
"js-cookie": "^3.0.5",
"next": "^14.2.25",
"next": "^14.2.26",
"postcss": "^8.4.38",
"react": "^18.2.0",
"react-dom": "^18.2.0",

View File

@@ -1,7 +1,7 @@
[global]
[hasura]
version = 'v2.25.1-ce'
version = 'v2.46.0-ce'
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
@@ -25,10 +25,10 @@ httpPoolSize = 100
[functions]
[functions.node]
version = 18
version = 22
[auth]
version = '0.24.0'
version = '0.38.0'
[auth.redirections]
clientUrl = 'http://localhost:3000'
@@ -139,12 +139,15 @@ timeout = 60000
enabled = false
[postgres]
version = '14.6-20230406-2'
version = '16.6-20250311-1'
[postgres.resources.storage]
capacity = 1
[provider]
[storage]
version = '0.5.1'
version = '0.7.1'
[observability]
[observability.grafana]

View File

@@ -1,5 +1,17 @@
---
## 0.8.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 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
## 0.7.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/sveltekit",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -16,7 +16,7 @@
"@playwright/test": "^1.41.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-vercel": "^5.6.3",
"@sveltejs/kit": "^2.11.1",
"@sveltejs/kit": "^2.20.6",
"@sveltejs/vite-plugin-svelte": "^5.0.2",
"@types/js-cookie": "^3.0.6",
"autoprefixer": "^10.4.19",
@@ -30,7 +30,7 @@
"svelte-check": "^3.6.8",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.3",
"vite": "^6.0.14",
"vite": "^6.2.7",
"vitest": "^0.25.8"
},
"type": "module",

View File

@@ -1,5 +1,24 @@
# @nhost-examples/react-apollo
## 1.6.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
### Patch Changes
- 97db637: fix: fix settings
## 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
## 1.5.0
### Minor Changes

View File

@@ -47,27 +47,6 @@ disableNewUsers = false
default = 'user'
allowed = ['user', 'me']
[auth.rateLimit]
[auth.rateLimit.emails]
limit = 100
interval = '1h'
[auth.rateLimit.sms]
limit = 100
interval = '1h'
[auth.rateLimit.bruteForce]
limit = 100
interval = '5m'
[auth.rateLimit.signups]
limit = 100
interval = '5m'
[auth.rateLimit.global]
limit = 1000
interval = '1m'
[auth.user.locale]
default = 'en'
allowed = ['en']
@@ -182,14 +161,6 @@ version = '16.6-20250311-1'
capacity = 1
[provider]
[provider.smtp]
host = "smtp.test.com"
method = "LOGIN"
password = "test123123"
port = 587
secure = false
sender = "test@nhost.io"
user = "test"
[storage]
version = '0.7.1'

View File

@@ -35,6 +35,32 @@
"op": "replace",
"path": "/auth/method/webauthn/relyingParty/origins/0"
},
{
"value": {
"bruteForce": {
"interval": "5m",
"limit": 100
},
"emails": {
"interval": "1h",
"limit": 100
},
"global": {
"interval": "1m",
"limit": 1000
},
"signups": {
"interval": "5m",
"limit": 100
},
"sms": {
"interval": "1h",
"limit": 100
}
},
"op": "add",
"path": "/auth/rateLimit"
},
{
"value": "http://localhost:3000",
"op": "replace",
@@ -77,5 +103,18 @@
"value": "http://localhost:3000",
"op": "replace",
"path": "/auth/redirections/clientUrl"
},
{
"value": {
"host": "smtp.test.com",
"method": "LOGIN",
"password": "test123123",
"port": 587,
"secure": false,
"sender": "test@nhost.io",
"user": "test"
},
"op": "add",
"path": "/provider/smtp"
}
]

View File

@@ -1,13 +1,14 @@
{
"name": "@nhost-examples/react-apollo",
"version": "1.5.0",
"version": "1.6.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"preview": "vite preview --port 3000",
"build:preview": "pnpm build && pnpm preview",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e": "pnpm e2e:start-backend && pnpm e2e:test",
"e2e:test": "pnpm install-browsers && pnpm playwright test",
@@ -37,7 +38,7 @@
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.52.2",
"react-router-dom": "^6.22.3",
"react-router-dom": "^7.5.2",
"sonner": "^1.5.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
@@ -63,6 +64,6 @@
"totp-generator": "^0.0.13",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.17"
"vite": "^5.4.19"
}
}

View File

@@ -16,11 +16,11 @@ export default defineConfig({
timeout: 5000
},
webServer: {
command: 'pnpm dev',
command: 'pnpm build:preview',
port: 3000
},
use: {
trace: 'on-first-retry',
trace: 'retain-on-failure',
baseURL: 'http://localhost:3000'
},
fullyParallel: true,

View File

@@ -1,5 +1,18 @@
# @nhost-examples/react-gqty
## 1.6.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 1.5.1
### Patch Changes
- d9eb906: fix: update vite and nextjs because of vulnerability
- @nhost/react@3.10.4
## 1.5.0
### Minor Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/react-gqty",
"private": true,
"version": "1.5.0",
"version": "1.6.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -27,6 +27,6 @@
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^4.9.5",
"vite": "^5.4.17"
"vite": "^5.4.19"
}
}

View File

@@ -1,5 +1,12 @@
# @nhost-examples/react-native
## 0.1.8
### Patch Changes
- @nhost/react@3.10.4
- @nhost/react-apollo@17.0.4
## 0.1.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-native",
"version": "0.1.7",
"version": "0.1.8",
"private": true,
"scripts": {
"android": "react-native run-android",

View File

@@ -1,5 +1,20 @@
# @nhost-examples/vue-apollo
## 0.12.0
### Minor Changes
- c97b43f: fix: update vite to address vulnerability audit
## 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
## 0.11.0
### Minor Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@nhost-examples/vue-apollo",
"private": true,
"version": "0.11.0",
"version": "0.12.0",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -37,7 +37,7 @@
"@xstate/inspect": "^0.6.5",
"sass": "1.86.1",
"typescript": "4.9.4",
"vite": "^5.4.17",
"vite": "^5.4.19",
"vue-tsc": "^0.38.9"
},
"eslintConfig": {

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