Compare commits

...

41 Commits

Author SHA1 Message Date
github-actions[bot]
60bcd8f949 chore: update versions (#2793)
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@7.1.3

### Patch Changes

-   @nhost/nhost-js@3.1.6

## @nhost/react-apollo@12.0.3

### Patch Changes

-   @nhost/apollo@7.1.3
-   @nhost/react@3.5.3

## @nhost/react-urql@9.0.3

### Patch Changes

-   @nhost/react@3.5.3

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

### Patch Changes

- e28975d: fix: refactor refreshTimer logic to avoid an infinite loop
when refreshToken has expired

## @nhost/nextjs@2.1.17

### Patch Changes

-   @nhost/react@3.5.3

## @nhost/nhost-js@3.1.6

### Patch Changes

-   Updated dependencies [e28975d]
    -   @nhost/hasura-auth-js@2.5.3

## @nhost/react@3.5.3

### Patch Changes

-   @nhost/nhost-js@3.1.6

## @nhost/vue@2.6.3

### Patch Changes

-   @nhost/nhost-js@3.1.6

## @nhost/dashboard@1.23.0

### Minor Changes

-   33284d3: fix: don't show double scrollbar in configuration editor

### Patch Changes

-   @nhost/react-apollo@12.0.3
-   @nhost/nextjs@2.1.17

## @nhost-examples/cli@0.3.8

### Patch Changes

-   @nhost/nhost-js@3.1.6

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

### Patch Changes

-   @nhost/react@3.5.3
-   @nhost/react-apollo@12.0.3

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

### Patch Changes

-   @nhost/react@3.5.3

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

### Patch Changes

-   @nhost/react@3.5.3
-   @nhost/react-urql@9.0.3

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

### Patch Changes

-   @nhost/nhost-js@3.1.6

## @nhost-examples/nextjs@0.3.8

### Patch Changes

-   @nhost/react@3.5.3
-   @nhost/react-apollo@12.0.3
-   @nhost/nextjs@2.1.17

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

### Patch Changes

-   @nhost/nhost-js@3.1.6

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

### Patch Changes

-   @nhost/nhost-js@3.1.6

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

### Patch Changes

-   @nhost/react@3.5.3
-   @nhost/react-apollo@12.0.3

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

### Patch Changes

-   @nhost/react@3.5.3

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

### Patch Changes

-   @nhost/react@3.5.3
-   @nhost/react-apollo@12.0.3

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

### Patch Changes

-   @nhost/nhost-js@3.1.6
-   @nhost/apollo@7.1.3
-   @nhost/vue@2.6.3

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

### Patch Changes

-   @nhost/apollo@7.1.3
-   @nhost/vue@2.6.3

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-09 10:08:51 +01:00
Hassan Ben Jobrane
e28975d6a5 fix(hasura-auth-js): refactor refreshTimer logic to avoid an infinite loop when refreshToken has expired (#2790)
### **User description**
fixes https://github.com/nhost/nhost/issues/2763
related https://github.com/nhost/nhost/issues/2635


___

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


___

### **Description**
- Refactored the `refreshTimer` logic in `createAuthMachine` to avoid an
infinite loop when the `refreshToken` has expired.
- Moved the condition checking if `expiresAt` is less than the current
time to ensure proper token refresh handling.
- Added a changeset file to document the patch update.



___



### **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>machine.ts</strong><dd><code>Refactor refreshTimer
logic to prevent infinite loop</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

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

<li>Refactored the <code>refreshTimer</code> logic to avoid an infinite
loop when the <br><code>refreshToken</code> has expired.<br> <li> Moved
the condition checking if <code>expiresAt</code> is less than the
current <br>time.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>small-trees-approve.md</strong><dd><code>Add changeset
for refreshTimer logic fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

.changeset/small-trees-approve.md

- Added a changeset file for the patch update.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-09 09:56:58 +01:00
Zephyr (David B.M.)
33284d3cf0 fix (dashboard): don't show double scrollbar in configuration editor (#2792)
Fixes #2789
2024-07-08 19:15:37 +02:00
github-actions[bot]
1dbd65eb0e chore: update versions (#2787)
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@1.22.0

### Minor Changes

-   998c037: fix: align drop-down list in select component
- 807b8c0: fix: show city name in region selection for project creation

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

### Patch Changes

- e3f0732: fix: add verify email button instead of doing an
auto-redirect

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-08 16:59:45 +01:00
Nuno Pato
6eec78f9c5 feat: dashboard: add support for zendesk (#2783)
### **PR Type**
Enhancement, Documentation


___

### **Description**
- Updated header to link to new support page.
- Added new `CommunityIcon`, `DiscordIcon`, and `EnvelopeIcon`
components.
- Created a new support page with links to documentation, GitHub issues,
and Discord community.
- Added a ticket creation page with a form for submitting support
tickets, integrated with Zendesk API.
- Added environment variables for Zendesk integration.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>9
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>Header.tsx</strong><dd><code>Update header to link to
new support page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/components/layout/Header/Header.tsx

<li>Removed <code>ContactUs</code> component and <code>Dropdown</code>
component.<br> <li> Added <code>NavLink</code> to <code>/support</code>
page.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>CommunityIcon.tsx</strong><dd><code>Add CommunityIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/components/ui/v2/icons/CommunityIcon/CommunityIcon.tsx

- Added new `CommunityIcon` component.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2783/files#diff-42970da68e2ef95e0aee273b264e69b21091866a9ba853fb594b08ab7e960ac1">+39/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export CommunityIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/components/ui/v2/icons/CommunityIcon/index.ts

- Exported `CommunityIcon` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>DiscordIcon.tsx</strong><dd><code>Add DiscordIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/components/ui/v2/icons/DiscordIcon/DiscordIcon.tsx

- Added new `DiscordIcon` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export DiscordIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

dashboard/src/components/ui/v2/icons/DiscordIcon/index.ts

- Exported `DiscordIcon` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>EnvelopeIcon.tsx</strong><dd><code>Add EnvelopeIcon
component</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/components/ui/v2/icons/EnvelopeIcon/EnvelopeIcon.tsx

- Added new `EnvelopeIcon` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export EnvelopeIcon
component</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;
</dd></summary>
<hr>

dashboard/src/components/ui/v2/icons/EnvelopeIcon/index.ts

- Exported `EnvelopeIcon` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.tsx</strong><dd><code>Add support page with
various help options</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/pages/support/index.tsx

<li>Added new support page with links to documentation, GitHub issues,
and <br>Discord community.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ticket.tsx</strong><dd><code>Add ticket creation page
with Zendesk integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/pages/support/ticket.tsx

<li>Added new ticket creation page with form for submitting support
<br>tickets.<br> <li> Integrated Zendesk API for ticket submission.<br>


</details>


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

</tr>                    
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>.env.example</strong><dd><code>Add Zendesk environment
variables</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/.env.example

- Added environment variables for Zendesk integration.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-07-05 16:20:57 +00:00
Hassan Ben Jobrane
e3f0732108 fix(react-apollo): add verify email button (#2782)
### **User description**
fixes https://github.com/nhost/nhost/issues/2741


___

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


___

### **Description**
- Added a `requestType` parameter to the `verifyEmail` function to
handle different types of email verification requests.
- Updated the email change test to include the `requestType` parameter.
- Replaced auto-redirect in the `VerifyPage` component with a
verification button and added error handling with notifications.
- Updated dependencies in `nhost.toml` to newer versions.
- Added a changeset file to document the email verification button
update.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>change-email.test.ts</strong><dd><code>Update email
change test with request type parameter</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/e2e/authenticated/change-email.test.ts

- Added `requestType` parameter to `verifyEmail` function call.



</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>utils.ts</strong><dd><code>Enhance email verification
utility with request type</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

examples/react-apollo/e2e/utils.ts

<li>Added <code>requestType</code> parameter to <code>verifyEmail</code>
function.<br> <li> Implemented conditional logic based on
<code>requestType</code>.<br> <li> Added button click for
verification.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Verify.tsx</strong><dd><code>Add verification button
and error handling in VerifyPage</code>&nbsp; </dd></summary>
<hr>

examples/react-apollo/src/Verify.tsx

<li>Replaced auto-redirect with a verification button.<br> <li> Added
error handling with notifications.<br> <li> Updated UI components for
verification.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2782/files#diff-b55b7fe9f71f1b4f1e1364b5ea5079241c87fe57f4a7c39d01039b43de725d7f">+21/-14</a>&nbsp;
</td>

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>gentle-brooms-flash.md</strong><dd><code>Add changeset
for email verification button update</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/gentle-brooms-flash.md

- Added changeset for email verification button update.



</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Dependencies</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>nhost.toml</strong><dd><code>Update dependencies in
nhost.toml</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

examples/react-apollo/nhost/nhost.toml

- Updated Hasura, Auth, and Postgres versions.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-05 15:31:15 +01:00
Zephyr (David B.M.)
807b8c049a fix (dashboard): show city name in region selector for project creation (#2788)
Fixes #2778
2024-07-05 12:46:38 +02:00
Zephyr (David B.M.)
998c0376bf fix (dashboard): align dropdown items in select component (#2786)
Fixes #2779
2024-07-05 12:02:18 +02:00
github-actions[bot]
cf5423dac6 chore: update versions (#2785)
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@1.21.0

### Minor Changes

- a2efeed: fix: improve project health error handling, add unknown state
and polling interval for health state

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-04 19:11:02 +01:00
Zephyr (David B.M.)
a2efeed36f fix (dashboard): improve project health error handling, add poll interval (#2780)
Fixes #2776
2024-07-04 19:45:31 +02:00
Hassan Ben Jobrane
533b74d82d chore: update pnpm/action-setup to v4 (#2784)
### **PR Type**
enhancement, configuration changes


___

### **Description**
- Updated `pnpm/action-setup` to version 4 in GitHub Actions
configuration.
- Updated `packageManager` to `pnpm@8.10.5` in `package.json`.



___



### **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>action.yaml</strong><dd><code>Update pnpm/action-setup
to v4 in GitHub Actions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.github/actions/install-dependencies/action.yaml

<li>Updated <code>pnpm/action-setup</code> version to v4.<br> <li> Set
<code>pnpm</code> version to 8.10.5.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update packageManager to
pnpm@8.10.5 in package.json</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

package.json

- Updated `packageManager` to `pnpm@8.10.5`.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-04 15:26:23 +01:00
github-actions[bot]
42cf86c8f1 chore: update versions (#2770)
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@1.20.0

### Minor Changes

- 8ea4210: fix: error toasts can be closed individually, instead of
dismissing all toasts at once
- 58919ba: chore: add blink animation when project health service is
updating

## @nhost/docs@2.14.2

### Patch Changes

-   86f3f8d: chore: fix broken link in react-native reference

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-03 11:31:48 +01:00
Zephyr (David B.M.)
70e74f2f3d dashboard: chore: error toast message improvements (#2773)
Closes #2726
2024-07-02 12:13:09 +02:00
Hassan Ben Jobrane
a01985466e chore(templates/react-native): fix ESLint configuration for react native template (#2774)
### **PR Type**
enhancement, bug fix


___

### **Description**
- Refactored multiple components and screens to remove semicolons and
adjust formatting for consistency.
- Added ESLint configuration file for the React Native template.
- Cleaned up Babel and React Native configuration files by removing
trailing commas.
- Updated pnpm lockfile to reflect new dependencies and versions.
- Bumped React Native template version to 0.0.14.
- Updated ESLint dependency version to 8.57.0.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>16
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>Button.tsx</strong><dd><code>Refactor Button component
for consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/Button.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-8c344335537207c9c9ae7fcc3ea55685d2724028d7e7b7bdf3b621e8cc9cf6bf">+16/-27</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ControlledInput.tsx</strong><dd><code>Refactor
ControlledInput component for consistent
formatting</code></dd></summary>
<hr>
      
templates/react-native/template/src/components/ControlledInput.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Drawer.tsx</strong><dd><code>Refactor Drawer component
for consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/Drawer.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-6f93136e2b891b6559a1ddf61fc3c60da972208f5cf0f44e7f0caa2247378626">+20/-31</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithAppleButton.tsx</strong><dd><code>Refactor
SignInWithAppleButton component for consistent
formatting</code></dd></summary>
<hr>
      
templates/react-native/template/src/components/SignInWithAppleButton.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithGoogleButton.tsx</strong><dd><code>Refactor
SignInWithGoogleButton component for consistent
formatting</code></dd></summary>
<hr>
      

templates/react-native/template/src/components/SignInWithGoogleButton.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-4ab8df68012aadbdb1a22916ee8586e2f914bd0443e80fd4db5b2ed83c2f3cd6">+10/-11</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>UploadFile.tsx</strong><dd><code>Refactor UploadFile
component for consistent formatting</code>&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/UploadFile.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-613fbf75a899e454b62df367e09d259d2966f196baaded5decf4cc1970eff45a">+24/-33</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>root.tsx</strong><dd><code>Clean up root component
imports</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/root.tsx

- Removed unused import statements.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Main.tsx</strong><dd><code>Refactor Main screen for
consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/Main.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-4f3b8aa2ebed081409e78c3f191963d260190458e79d7e92565ad5ea70c02316">+72/-60</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Profile.tsx</strong><dd><code>Refactor Profile screen
for consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/Profile.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-085837f3d86a1d36fb4dae5d15329e6353c7804cf6655270167484c33362e46f">+13/-20</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignIn.tsx</strong><dd><code>Refactor SignIn screen for
consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/SignIn.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-316ef2753576fb251278ccc28461eaed1f57023f1ae4338c6a00d224d51a5f21">+50/-72</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignUp.tsx</strong><dd><code>Refactor SignUp screen for
consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/SignUp.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-a08ab32eea3633f66c839543d7acc88aca48f9af1760d23d1ea517bce00f4a5a">+37/-58</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Storage.tsx</strong><dd><code>Refactor Storage screen
for consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/Storage.tsx

<li>Removed semicolons and adjusted formatting.<br> <li> Simplified JSX
structure.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>babel.config.js</strong><dd><code>Clean up Babel
configuration file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/babel.config.js

- Removed trailing commas.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.js</strong><dd><code>Refactor index.js for
consistent formatting</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/index.js

- Removed semicolons and adjusted formatting.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>react-native.config.js</strong><dd><code>Clean up React
Native configuration file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/react-native.config.js

- Removed trailing commas.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Bump React Native
template version to 0.0.14</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/package.json

- Updated template version to 0.0.14.



</details>
    

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

</tr>                    
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>.eslintrc.js</strong><dd><code>Add ESLint configuration
for React Native template</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/.eslintrc.js

- Added ESLint configuration file.



</details>
    

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

</tr>                    

</table></details></td></tr><tr><td><strong>Dependencies</strong></td><td><details><summary>2
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>pnpm-lock.yaml</strong><dd><code>Update pnpm
lockfile</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; &nbsp;
</dd></summary>
<hr>
      
pnpm-lock.yaml

- Updated lockfile to reflect new dependencies and versions.



</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2774/files#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bb">+83/-77</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update ESLint dependency
version</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/package.json

- Updated ESLint version to 8.57.0.



</details>
    

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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-01 18:31:43 +01:00
Zephyr (David B.M.)
8ea4210582 dashboard: fix: close error toasts individually (#2772)
Fixes #2656
2024-06-28 13:23:15 +02:00
Zephyr (David B.M.)
58919ba763 dashboard: chore: add blink animation to project health when service is updating (#2771) 2024-06-28 11:23:00 +02:00
Hassan Ben Jobrane
86f3f8d505 chore(docs): fix broken link in react-native reference (#2769)
### **PR Type**
Documentation


___

### **Description**
- Fixed a broken link in the React Native support documentation.
- Added a changeset file to document the fix.



___



### **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>shy-timers-turn.md</strong><dd><code>Add changeset for
documentation update</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
.changeset/shy-timers-turn.md

- Added a changeset file for documenting the fix.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>support.mdx</strong><dd><code>Fix broken link in React
Native support documentation</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
docs/reference/react-native/support.mdx

- Fixed a broken link in the React Native support documentation.



</details>
    

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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-06-28 09:53:37 +01:00
github-actions[bot]
201abb89fd chore: update versions (#2768)
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@1.19.0

### Minor Changes

- b519862: fix: get configuration in configuration editor using local
development environment

## @nhost/docs@2.14.1

### Patch Changes

- cf6b712: chore: add pnpm to list of supported packagers for functions

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-28 08:52:00 +02:00
David Barroso
cf6b712b20 chore (docs): add pnpm to list of supported packagers for functions (#2766) 2024-06-28 08:41:08 +02:00
Zephyr (David B.M.)
b51986289d dashboard: fix: get settings in toml editor with local development backend (#2767) 2024-06-27 15:31:55 +02:00
github-actions[bot]
c640c50c70 chore: update versions (#2758)
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@1.18.0

### Minor Changes

- 502abad: feat: add services health checks indicators to the overview
page
-   b3ff6ad: chore: update title text on service status modal
- dbadf59: feat: add project configuration TOML editor to the settings
page

## @nhost/docs@2.14.0

### Minor Changes

-   79ce7ca: feat: add react-native quickstart guide
-   bedbb82: feat: functions: added runtime/pkg manager information

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

### Patch Changes

- 9c9137f: fix: disable autoRefreshToken when running nhost server side

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-27 13:23:39 +02:00
Zephyr (David B.M.)
b3ff6adcc2 dashboard: chore: project health change modal title (#2765)
Change modal title from 'Service logs' to 'Service State'
2024-06-27 12:05:57 +02:00
Zephyr (David B.M.)
dbadf59092 dashboard: feat: raw TOML editor (#2752)
### **User description**
Closes #2727


___

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


___

### **Description**
- Introduced a new TOML editor component for editing project
configurations.
- Added new GraphQL queries and mutations to handle raw JSON
configurations.
- Updated SettingsSidebar and SettingsLayout components for better
layout and navigation.
- Added new SlidersIcon component and integrated it into the settings
navigation.
- Updated dependencies to support new features.
- Fixed various layout and style issues in the settings components.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement
</strong></td><td><details><summary>10 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>SettingsLayout.tsx</strong><dd><code>Adjust overflow
behavior in SettingsLayout component</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
dashboard/src/components/layout/SettingsLayout/SettingsLayout.tsx

- Changed overflow behavior for better vertical scrolling.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SettingsSidebar.tsx</strong><dd><code>Update
SettingsSidebar layout and styles</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
dashboard/src/components/layout/SettingsSidebar/SettingsSidebar.tsx

<li>Added new <code>SlidersIcon</code> import.<br> <li> Introduced
<code>textClassName</code> prop to <code>SettingsNavLink</code>.<br>
<li> Updated layout and styles for better alignment and spacing.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2752/files#diff-228be719ea3624edbfd2af99af3c076cebb3d0732026987306aa1032a795ba00">+23/-3</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SlidersIcon.tsx</strong><dd><code>Add SlidersIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
dashboard/src/components/ui/v2/icons/SlidersIcon/SlidersIcon.tsx

- Added new `SlidersIcon` component.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export SlidersIcon
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
dashboard/src/components/ui/v2/icons/SlidersIcon/index.ts

- Exported `SlidersIcon` component.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>TOMLEditor.tsx</strong><dd><code>Add TOMLEditor
component for TOML configurations</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      

dashboard/src/features/projects/common/components/settings/TOMLEditor/TOMLEditor.tsx

<li>Added new <code>TOMLEditor</code> component for editing TOML
configurations.<br> <li> Implemented TOML parsing and error
handling.<br> <li> Added save and revert functionality.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.ts</strong><dd><code>Export TOMLEditor
component</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      

dashboard/src/features/projects/common/components/settings/TOMLEditor/index.ts

- Exported `TOMLEditor` component.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.tsx</strong><dd><code>Add TOML editor page under
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>
      
dashboard/src/pages/[workspaceSlug]/[appSlug]/settings/editor/index.tsx

- Added new page for TOML editor under settings.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>graphql.ts</strong><dd><code>Add GraphQL queries and
mutations for raw JSON configurations</code></dd></summary>
<hr>
      
dashboard/src/utils/__generated__/graphql.ts

<li>Added new GraphQL queries and mutations for handling raw JSON
<br>configurations.<br> <li> Updated types and schema for new
features.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>getConfigRawJSON.graphql</strong><dd><code>Add GraphQL
query for fetching raw JSON configuration</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
dashboard/src/gql/app/settings/getConfigRawJSON.graphql

- Added GraphQL query for fetching raw JSON configuration.



</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>replaceConfigRawJSON.graphql</strong><dd><code>Add
GraphQL mutation for replacing raw JSON
configuration</code></dd></summary>
<hr>
      
dashboard/src/gql/app/settings/replaceConfigRawJSON.graphql

- Added GraphQL mutation for replacing raw JSON configuration.



</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2752/files#diff-5d49b4c5e9fa194a02a0a974c245f8c50faf437d6becadf7947ee73083ac8374">+3/-0</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>
    <details>
<summary><strong>tidy-shirts-kneel.md</strong><dd><code>Add changeset
for TOML editor feature</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
.changeset/tidy-shirts-kneel.md

- Added changeset for TOML editor feature.



</details>
    

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

</tr>                    
</table></details></td></tr><tr><td><strong>Dependencies
</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>package.json</strong><dd><code>Update dependencies for
TOML editor</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>
      
dashboard/package.json

<li>Added new dependencies for TOML parsing and CodeMirror
integration.<br> <li> Updated existing dependencies.<br>


</details>
    

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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-06-26 19:35:43 +02:00
David Barroso
bedbb82cd7 feat (docs): functions: added runtime/pkg manager information (#2761) 2024-06-26 12:06:09 +02:00
Hassan Ben Jobrane
79ce7cae2f feat: react-native support (#2675)
### **User description**
resolves https://github.com/nhost/projects/issues/78


___

### **PR Type**
enhancement, documentation, tests


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement
</strong></td><td><details><summary>39 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>SignIn.tsx</strong><dd><code>Add SignIn screen with
OAuth and email/password sign-in</code>&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/SignIn.tsx

<li>Added SignIn screen component with form handling and OAuth
<br>integration.<br> <li> Implemented email/password sign-in and OAuth
sign-in with Apple and <br>Google.<br> <li> Added navigation to SignUp
screen.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-316ef2753576fb251278ccc28461eaed1f57023f1ae4338c6a00d224d51a5f21">+167/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignIn.tsx</strong><dd><code>Add SignIn screen with
OAuth and email/password sign-in</code>&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/screens/SignIn.tsx

<li>Added SignIn screen component with form handling and OAuth
<br>integration.<br> <li> Implemented email/password sign-in and OAuth
sign-in with Apple and <br>Google.<br> <li> Added navigation to SignUp
screen.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignUp.tsx</strong><dd><code>Add SignUp screen with
email/password sign-up</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/SignUp.tsx

<li>Added SignUp screen component with form handling.<br> <li>
Implemented email/password sign-up with email verification.<br> <li>
Added navigation to SignIn screen.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignUp.tsx</strong><dd><code>Add SignUp screen with
email/password sign-up</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/screens/SignUp.tsx

<li>Added SignUp screen component with form handling.<br> <li>
Implemented email/password sign-up with email verification.<br> <li>
Added navigation to SignIn screen.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-18516699ddb4e9d0f5a2a78cd75502a8209fb7a85c5d2db4b65bea895b35a944">+142/-0</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Main.tsx</strong><dd><code>Add Main screen with Drawer
and Stack navigators</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/screens/Main.tsx

<li>Added Main screen component with Drawer and Stack navigators.<br>
<li> Integrated authentication status check.<br> <li> Added navigation
for Profile, Todos, and Storage screens.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Main.tsx</strong><dd><code>Add Main screen with Drawer
and Stack navigators</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/Main.tsx

<li>Added Main screen component with Drawer and Stack navigators.<br>
<li> Integrated authentication status check.<br> <li> Added navigation
for Profile, Todos, and Storage screens.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>UploadFile.tsx</strong><dd><code>Add UploadFile
component for file uploads</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/UploadFile.tsx

<li>Added UploadFile component for file uploads.<br> <li> Integrated
Nhost storage for file handling.<br> <li> Implemented file picker and
upload status display.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-613fbf75a899e454b62df367e09d259d2966f196baaded5decf4cc1970eff45a">+82/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>UploadFile.tsx</strong><dd><code>Add UploadFile
component for file uploads</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
examples/react_native/src/components/UploadFile.tsx

<li>Added UploadFile component for file uploads.<br> <li> Integrated
Nhost storage for file handling.<br> <li> Implemented file picker and
upload status display.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-412d24720f24a956f358f59dfd48bb86daf1eba2fa017f9dfd7a0f96e8114691">+82/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>AddTodoForm.tsx</strong><dd><code>Add AddTodoForm
component for adding new todos</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/components/AddTodoForm.tsx

<li>Added AddTodoForm component for adding new todos.<br> <li>
Integrated GraphQL mutation for adding todos.<br> <li> Implemented form
handling and validation.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-46d3c5a1ab5e8cde7e04d78a55fdcf04bbee77795b78038f15e61882bb10b069">+64/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Button.tsx</strong><dd><code>Add Button component with
loading state</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/Button.tsx

<li>Added Button component with loading state.<br> <li> Implemented
customizable styles and labels.<br> <li> Integrated with Pressable for
button interactions.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-8c344335537207c9c9ae7fcc3ea55685d2724028d7e7b7bdf3b621e8cc9cf6bf">+67/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Button.tsx</strong><dd><code>Add Button component with
loading state</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
examples/react_native/src/components/Button.tsx

<li>Added Button component with loading state.<br> <li> Implemented
customizable styles and labels.<br> <li> Integrated with Pressable for
button interactions.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Todo.tsx</strong><dd><code>Add Todo component for
displaying and deleting todos</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/src/components/Todo.tsx

<li>Added Todo component for displaying individual todos.<br> <li>
Integrated GraphQL mutation for deleting todos.<br> <li> Implemented
delete button with confirmation.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Todos.tsx</strong><dd><code>Add Todos screen for
listing and managing todos</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/screens/Todos.tsx

<li>Added Todos screen for listing all todos.<br> <li> Integrated
GraphQL query for fetching todos.<br> <li> Implemented AddTodoForm and
Todo components.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Drawer.tsx</strong><dd><code>Add Drawer component for
navigation</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>
      
templates/react-native/template/src/components/Drawer.tsx

<li>Added Drawer component for navigation.<br> <li> Integrated Nhost
client for sign-out functionality.<br> <li> Implemented custom drawer
content.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Drawer.tsx</strong><dd><code>Add Drawer component for
navigation</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_native/src/components/Drawer.tsx

<li>Added Drawer component for navigation.<br> <li> Integrated Nhost
client for sign-out functionality.<br> <li> Implemented custom drawer
content.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Profile.tsx</strong><dd><code>Add Profile screen for
displaying user information</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/screens/Profile.tsx

<li>Added Profile screen for displaying user information.<br> <li>
Integrated Nhost client for fetching user data.<br> <li> Implemented
scrollable view for user details.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-085837f3d86a1d36fb4dae5d15329e6353c7804cf6655270167484c33362e46f">+47/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Profile.tsx</strong><dd><code>Add Profile screen for
displaying user information</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/screens/Profile.tsx

<li>Added Profile screen for displaying user information.<br> <li>
Integrated Nhost client for fetching user data.<br> <li> Implemented
scrollable view for user details.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ControlledInput.tsx</strong><dd><code>Add
ControlledInput component for form inputs</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
templates/react-native/template/src/components/ControlledInput.tsx

<li>Added ControlledInput component for form inputs.<br> <li> Integrated
react-hook-form for form handling.<br> <li> Implemented customizable
input styles.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ControlledInput.tsx</strong><dd><code>Add
ControlledInput component for form inputs</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/src/components/ControlledInput.tsx

<li>Added ControlledInput component for form inputs.<br> <li> Integrated
react-hook-form for form handling.<br> <li> Implemented customizable
input styles.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-909a89de58e3629f91cf5a942f8065155568a9c222a362f380026f3150588684">+36/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithAppleButton.tsx</strong><dd><code>Add
SignInWithAppleButton component for Apple OAuth</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/src/components/SignInWithAppleButton.tsx

<li>Added SignInWithAppleButton component for Apple OAuth.<br> <li>
Integrated with Button component for styling.<br> <li> Implemented OAuth
sign-in functionality.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithAppleButton.tsx</strong><dd><code>Add
SignInWithAppleButton component for Apple OAuth</code>&nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/components/SignInWithAppleButton.tsx

<li>Added SignInWithAppleButton component for Apple OAuth.<br> <li>
Integrated with Button component for styling.<br> <li> Implemented OAuth
sign-in functionality.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithGoogleButton.tsx</strong><dd><code>Add
SignInWithGoogleButton component for Google OAuth</code>&nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      

templates/react-native/template/src/components/SignInWithGoogleButton.tsx

<li>Added SignInWithGoogleButton component for Google OAuth.<br> <li>
Integrated with Button component for styling.<br> <li> Implemented OAuth
sign-in functionality.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>SignInWithGoogleButton.tsx</strong><dd><code>Add
SignInWithGoogleButton component for Google OAuth</code>&nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/components/SignInWithGoogleButton.tsx

<li>Added SignInWithGoogleButton component for Google OAuth.<br> <li>
Integrated with Button component for styling.<br> <li> Implemented OAuth
sign-in functionality.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-402a88447117f62af1fd0ff8431605f8e3e59f1bac907583cde54257fe259cf3">+36/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>root.tsx</strong><dd><code>Add root component for Nhost
integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
templates/react-native/template/src/root.tsx

<li>Added root component for Nhost integration.<br> <li> Configured
Nhost client with Apollo provider.<br> <li> Implemented Main component
as the entry point.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-269180ad03ed042f1ba8b7b6d829cdd6f14f142a4f0c2de8129535431414a20b">+25/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>root.tsx</strong><dd><code>Add root component for Nhost
integration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/src/root.tsx

<li>Added root component for Nhost integration.<br> <li> Configured
Nhost client with Apollo provider.<br> <li> Implemented Main component
as the entry point.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>todos.ts</strong><dd><code>Add GraphQL queries and
mutations for todos</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/src/graphql/todos.ts

<li>Added GraphQL queries and mutations for todos.<br> <li> Implemented
queries for fetching and managing todos.<br> <li> Integrated with Apollo
client.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-2861d3d57db0e3186e85de60ab72862b3a285cea59266912c937cb592fd6d286">+27/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Storage.tsx</strong><dd><code>Add Storage screen for
file uploads</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>
      
templates/react-native/template/src/screens/Storage.tsx

<li>Added Storage screen for file uploads.<br> <li> Integrated
UploadFile component.<br> <li> Implemented basic layout and styles.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>Storage.tsx</strong><dd><code>Add Storage screen for
file uploads</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_native/src/screens/Storage.tsx

<li>Added Storage screen for file uploads.<br> <li> Integrated
UploadFile component.<br> <li> Implemented basic layout and styles.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.js</strong><dd><code>Add entry point for React
Native application</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/index.js

<li>Added entry point for React Native application.<br> <li> Configured
AppRegistry with root component.<br> <li> Implemented base64 decode
polyfill.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>index.js</strong><dd><code>Add entry point for React
Native application</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/index.js

<li>Added entry point for React Native application.<br> <li> Configured
AppRegistry with root component.<br> <li> Implemented base64 decode
polyfill.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-45924a6e131cd483dae95ab9b4d63680483383bf1d76d0fcfe03792f7f49b223">+8/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
confirming email change in Bulgarian</code></dd></summary>
<hr>
      

examples/react_native/backend/nhost/emails/bg/email-confirm-change/body.html

<li>Added email template for confirming email change in Bulgarian.<br>
<li> Included link for email confirmation.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
verifying email in Bulgarian</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>
      
examples/react_native/backend/nhost/emails/bg/email-verify/body.html

<li>Added email template for verifying email in Bulgarian.<br> <li>
Included link for email verification.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-9694390532932bf6b3b05aeeaee34fae82e5b5fe8a276053498fed6d299d7a5f">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
passwordless sign-in in Bulgarian</code>&nbsp; </dd></summary>
<hr>
      

examples/react_native/backend/nhost/emails/bg/signin-passwordless/body.html

<li>Added email template for passwordless sign-in in Bulgarian.<br> <li>
Included magic link for sign-in.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
verifying email in Czech</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/backend/nhost/emails/cs/email-verify/body.html

<li>Added email template for verifying email in Czech.<br> <li> Included
link for email verification.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-31a20325521b9fd39c524d1a8a5e3aed4def614a49df8eb633a9a40ad28c5d90">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
password reset in Bulgarian</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/backend/nhost/emails/bg/password-reset/body.html

<li>Added email template for password reset in Bulgarian.<br> <li>
Included link for password reset.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-23d5b1b5d3a7373015a1f68049a0fe0aefd5fe556a5b3278601a5949eea6d993">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
confirming email change in Czech</code>&nbsp; &nbsp; </dd></summary>
<hr>
      

examples/react_native/backend/nhost/emails/cs/email-confirm-change/body.html

<li>Added email template for confirming email change in Czech.<br> <li>
Included link for email confirmation.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
passwordless sign-in in Czech</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      

examples/react_native/backend/nhost/emails/cs/signin-passwordless/body.html

<li>Added email template for passwordless sign-in in Czech.<br> <li>
Included magic link for sign-in.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
password reset in French</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/backend/nhost/emails/fr/password-reset/body.html

<li>Added email template for password reset in French.<br> <li> Included
link for password reset.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-84f58a32f2ac4ef946c541166b7dab37a053ae05e9b0b4ce98d07ba0617072ef">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>body.html</strong><dd><code>Add email template for
passwordless sign-in in French</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      

examples/react_native/backend/nhost/emails/fr/signin-passwordless/body.html

<li>Added email template for passwordless sign-in in French.<br> <li>
Included magic link for sign-in.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-900e63eb1054e9b18941e7bd2719baf3e0bae8fe57c34423202110e0b8070e06">+18/-0</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></details></td></tr><tr><td><strong>Tests
</strong></td><td><details><summary>3 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>App.test.tsx</strong><dd><code>Add test for App
component rendering</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>
      
examples/react_native/__tests__/App.test.tsx

<li>Added test for App component rendering.<br> <li> Integrated
MockedProvider for Apollo client.<br> <li> Implemented basic snapshot
test.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>BroadcastChannel.js</strong><dd><code>Add mock
implementation for BroadcastChannel</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/__mocks__/BroadcastChannel.js

<li>Added mock implementation for BroadcastChannel.<br> <li> Implemented
basic methods for testing.<br> <li> Integrated with Jest setup.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>setup-jest.js</strong><dd><code>Add Jest setup file for
React Native</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>
      
examples/react_native/setup-jest.js

<li>Added Jest setup file for React Native.<br> <li> Configured mocks
for AsyncStorage and DocumentPicker.<br> <li> Integrated
MockBroadcastChannel for testing.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-399a7e8ebafa633b7a4bb8ecd305506f3ca408a14b0656119edb2f92c3737058">+22/-0</a>&nbsp;
&nbsp; </td>

</tr>                    
</table></details></td></tr><tr><td><strong>Configuration changes
</strong></td><td><details><summary>10 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>metro.config.js</strong><dd><code>Add Metro
configuration for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/metro.config.js

<li>Added Metro configuration for React Native.<br> <li> Configured
watch folders and resolver paths.<br> <li> Implemented middleware for
asset path correction.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-0321acc90135539c65928164f29f7ce016c888d40bfac454b121af72aee34cec">+35/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>babel.config.js</strong><dd><code>Add Babel
configuration for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/babel.config.js

<li>Added Babel configuration for React Native.<br> <li> Configured
presets and plugins for module resolution.<br> <li> Integrated React
Native Reanimated plugin.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>babel.config.js</strong><dd><code>Add Babel
configuration for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/babel.config.js

<li>Added Babel configuration for React Native.<br> <li> Configured
presets and plugins for module resolution.<br> <li> Integrated React
Native Reanimated plugin.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>metro.config.js</strong><dd><code>Add Metro
configuration for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template/metro.config.js

<li>Added Metro configuration for React Native.<br> <li> Configured
watch folders and resolver paths.<br> <li> Implemented middleware for
asset path correction.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>.prettierrc.js</strong><dd><code>Add Prettier
configuration file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/.prettierrc.js

<li>Added Prettier configuration file.<br> <li> Configured formatting
rules for the project.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>jest.config.js</strong><dd><code>Add Jest configuration
file for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
examples/react_native/jest.config.js

<li>Added Jest configuration file for React Native.<br> <li> Configured
preset and transform ignore patterns.<br> <li> Integrated setup file for
Jest.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>react-native.config.js</strong><dd><code>Add React
Native configuration file</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>
      
templates/react-native/template/react-native.config.js

<li>Added React Native configuration file.<br> <li> Configured
dependency settings for vector icons.<br>


</details>
    

  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2675/files#diff-6354847cf389c8910c93cc252657e572168b8dd1da7896f5b8bc91bf62bc5855">+9/-0</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>react-native.config.js</strong><dd><code>Add React
Native configuration file</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_native/react-native.config.js

<li>Added React Native configuration file.<br> <li> Configured
dependency settings for vector icons.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>template.config.js</strong><dd><code>Add template
configuration file for React Native</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
templates/react-native/template.config.js

<li>Added template configuration file for React Native.<br> <li>
Configured placeholder name and template directory.<br>


</details>
    

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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>.eslintrc.js</strong><dd><code>Add ESLint configuration
file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>
      
examples/react_native/.eslintrc.js

<li>Added ESLint configuration file.<br> <li> Extended React Native
ESLint configuration.<br>


</details>
    

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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-06-25 14:12:25 +01:00
Hassan Ben Jobrane
9c9137f813 fix: quickstarts: next-js-server-components: disable autoRefreshToken when running nhost-js server side (#2760)
### **User description**
fixes https://github.com/nhost/nhost/issues/2742


___

### **PR Type**
Bug fix, Documentation


___

### **Description**
- Disabled `autoRefreshToken` in the server-side configuration of
`NhostClient` to fix an issue.
- Added a changeset to document the fix.



___



### **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>nhost.ts</strong><dd><code>Disable autoRefreshToken in
server-side NhostClient configuration</code></dd></summary>
<hr>
      
examples/quickstarts/nextjs-server-components/src/utils/nhost.ts

<li>Disabled <code>autoRefreshToken</code> when running server side.<br>
<li> Added <code>autoRefreshToken: false</code> to
<code>NhostClient</code> configuration.<br>


</details>
    

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

</tr>                    
</table></td></tr><tr><td><strong>Documentation
</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>real-pens-fly.md</strong><dd><code>Document changeset
for disabling autoRefreshToken</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>
      
.changeset/real-pens-fly.md

- Added changeset documentation for disabling `autoRefreshToken`.



</details>
    

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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-06-25 13:51:20 +01:00
Zephyr (David B.M.)
502abadbae feat(dashboard): project health (#2731) 2024-06-20 17:24:19 +02:00
github-actions[bot]
b6b67773d1 chore: update versions (#2756)
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/google-translation@0.2.1

### Patch Changes

-   33ce955: chore: update @google-cloud/translate dep to v8.3.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-18 15:40:14 +01:00
Hassan Ben Jobrane
33ce95536d chore: fix vulnerabilities (#2755) 2024-06-18 14:41:58 +01:00
github-actions[bot]
11f9ed7507 chore: update versions (#2739)
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@1.17.0

### Minor Changes

- 77fba27: fix: postgres version validation when activating ai in ai
settings page
-   ac6d1b6: feat: use name instead of awsName

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-05 11:07:59 +01:00
David Barroso
ac6d1b6e01 feat (dashboard): use name instead of awsName (#2745) 2024-06-05 10:55:42 +01:00
Zephyr (David B.M.)
77fba27d12 fix (dashboard): validate postgres version in ai service settings page (#2735) 2024-05-31 12:38:36 +02:00
github-actions[bot]
7163854767 chore: update versions (#2724)
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@7.1.2

### Patch Changes

-   @nhost/nhost-js@3.1.5

## @nhost/react-apollo@12.0.2

### Patch Changes

-   @nhost/apollo@7.1.2
-   @nhost/react@3.5.2

## @nhost/react-urql@9.0.2

### Patch Changes

-   @nhost/react@3.5.2

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

### Patch Changes

- a03fb2c: fix: deep clone machine context to prevent mutations in
nested objects during initial session setup

## @nhost/nextjs@2.1.16

### Patch Changes

-   @nhost/react@3.5.2

## @nhost/nhost-js@3.1.5

### Patch Changes

-   Updated dependencies [a03fb2c]
    -   @nhost/hasura-auth-js@2.5.2

## @nhost/react@3.5.2

### Patch Changes

-   @nhost/nhost-js@3.1.5

## @nhost/vue@2.6.2

### Patch Changes

-   @nhost/nhost-js@3.1.5

## @nhost/docs@2.13.0

### Minor Changes

-   6fb0cc2: fix: minor improvements to compute resources' docs
-   66bd450: chore: various improvements

## @nhost/dashboard@1.16.3

### Patch Changes

- 87a37cf: fix: remove unnecessary isPlatform check from verify button
disable logic on custom domains
    -   @nhost/react-apollo@12.0.2
    -   @nhost/nextjs@2.1.16

## @nhost-examples/cli@0.3.7

### Patch Changes

-   @nhost/nhost-js@3.1.5

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

### Patch Changes

-   @nhost/react@3.5.2
-   @nhost/react-apollo@12.0.2

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

### Patch Changes

-   @nhost/react@3.5.2

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

### Patch Changes

-   @nhost/react@3.5.2
-   @nhost/react-urql@9.0.2

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

### Patch Changes

-   @nhost/nhost-js@3.1.5

## @nhost-examples/nextjs@0.3.7

### Patch Changes

-   @nhost/react@3.5.2
-   @nhost/react-apollo@12.0.2
-   @nhost/nextjs@2.1.16

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

### Patch Changes

-   @nhost/nhost-js@3.1.5

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

### Patch Changes

-   @nhost/nhost-js@3.1.5

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

### Patch Changes

-   @nhost/react@3.5.2
-   @nhost/react-apollo@12.0.2

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

### Patch Changes

-   @nhost/react@3.5.2

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

### Patch Changes

-   @nhost/nhost-js@3.1.5
-   @nhost/apollo@7.1.2
-   @nhost/vue@2.6.2

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

### Patch Changes

-   @nhost/apollo@7.1.2
-   @nhost/vue@2.6.2

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-31 10:25:11 +01:00
David Barroso
66bd4504d7 chore (docs): various improvements (#2738)
- Adds references to custom domains in social signin methods
- a short note to remind users to update the SDK instantiation when
using custom domains
- added a sample function to generate custom jwts
2024-05-31 11:22:11 +02:00
Hassan Ben Jobrane
a03fb2cf82 fix(hasura-auth-js): deep clone machine context to avoid mutating nested objects (#2736)
fixes https://github.com/nhost/nhost/issues/2732
2024-05-31 10:20:18 +01:00
Hassan Ben Jobrane
87a37cfc08 fix(dashboard): remove isPlatform check from verify button disable logic (#2737) 2024-05-31 09:54:55 +01:00
David Barroso
6fb0cc27aa fix (docs): improvements to compute resources (#2723) 2024-05-23 16:19:36 +02:00
github-actions[bot]
2c33051f83 chore: update versions (#2719)
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@7.1.1

### Patch Changes

-   @nhost/nhost-js@3.1.4

## @nhost/react-apollo@12.0.1

### Patch Changes

-   @nhost/apollo@7.1.1
-   @nhost/react@3.5.1

## @nhost/react-urql@9.0.1

### Patch Changes

-   @nhost/react@3.5.1

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

### Patch Changes

- f4f0353: fix: improve environment/browser detection to support React
Native
- defffd8: fix: resolve issue where `/token` endpoint is called with an
empty token during sign-in

## @nhost/nextjs@2.1.15

### Patch Changes

-   @nhost/react@3.5.1

## @nhost/nhost-js@3.1.4

### Patch Changes

-   Updated dependencies [f4f0353]
-   Updated dependencies [defffd8]
    -   @nhost/hasura-auth-js@2.5.1

## @nhost/react@3.5.1

### Patch Changes

-   @nhost/nhost-js@3.1.4

## @nhost/vue@2.6.1

### Patch Changes

-   @nhost/nhost-js@3.1.4

## @nhost/dashboard@1.16.2

### Patch Changes

- a9413af: fix: update `GetAllWorkspacesAndProjects` query polling to
use exponential backoff
    -   @nhost/react-apollo@12.0.1
    -   @nhost/nextjs@2.1.15

## @nhost-examples/cli@0.3.6

### Patch Changes

-   @nhost/nhost-js@3.1.4

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

### Patch Changes

-   @nhost/react@3.5.1
-   @nhost/react-apollo@12.0.1

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

### Patch Changes

-   @nhost/react@3.5.1

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

### Patch Changes

-   @nhost/react@3.5.1
-   @nhost/react-urql@9.0.1

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

### Patch Changes

-   @nhost/nhost-js@3.1.4

## @nhost-examples/nextjs@0.3.6

### Patch Changes

-   @nhost/react@3.5.1
-   @nhost/react-apollo@12.0.1
-   @nhost/nextjs@2.1.15

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

### Patch Changes

-   @nhost/nhost-js@3.1.4

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

### Patch Changes

-   @nhost/nhost-js@3.1.4

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

### Patch Changes

-   @nhost/react@3.5.1
-   @nhost/react-apollo@12.0.1

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

### Patch Changes

-   @nhost/react@3.5.1

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

### Patch Changes

-   @nhost/nhost-js@3.1.4
-   @nhost/apollo@7.1.1
-   @nhost/vue@2.6.1

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

### Patch Changes

-   @nhost/apollo@7.1.1
-   @nhost/vue@2.6.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-21 15:23:32 +01:00
Hassan Ben Jobrane
a9413af6e0 fix: update GetAllWorkspacesAndProjects query polling to use exp backoff (#2718) 2024-05-21 13:44:38 +01:00
Hassan Ben Jobrane
f4f0353f2e fix(hasura-auth-js): resolve social sign-in issue in React Native (#2716)
fix https://github.com/nhost/nhost/issues/2216
2024-05-20 19:13:13 +01:00
Hassan Ben Jobrane
defffd8bc4 fix: update internal-client to use payload.token instead of payload.token.data (#2717) 2024-05-20 19:05:21 +01:00
426 changed files with 24321 additions and 3391 deletions

View File

@@ -12,7 +12,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v4
with:
version: 8.10.5
run_install: false

View File

@@ -17,5 +17,10 @@ NEXT_PUBLIC_GITHUB_APP_INSTALL_URL=<github_app_install_url>
NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET=<nhost_bragi_websocket>
NEXT_PUBLIC_ZENDESK_URL=
NEXT_PUBLIC_ZENDESK_API_KEY=
NEXT_PUBLIC_ZENDESK_USER_EMAIL=
CODEGEN_GRAPHQL_URL=https://local.graphql.nhost.run/v1
CODEGEN_HASURA_ADMIN_SECRET=nhost-admin-secret

View File

@@ -1,5 +1,73 @@
# @nhost/dashboard
## 1.23.0
### Minor Changes
- 33284d3: fix: don't show double scrollbar in configuration editor
### Patch Changes
- @nhost/react-apollo@12.0.3
- @nhost/nextjs@2.1.17
## 1.22.0
### Minor Changes
- 998c037: fix: align drop-down list in select component
- 807b8c0: fix: show city name in region selection for project creation
## 1.21.0
### Minor Changes
- a2efeed: fix: improve project health error handling, add unknown state and polling interval for health state
## 1.20.0
### Minor Changes
- 8ea4210: fix: error toasts can be closed individually, instead of dismissing all toasts at once
- 58919ba: chore: add blink animation when project health service is updating
## 1.19.0
### Minor Changes
- b519862: fix: get configuration in configuration editor using local development environment
## 1.18.0
### Minor Changes
- 502abad: feat: add services health checks indicators to the overview page
- b3ff6ad: chore: update title text on service status modal
- dbadf59: feat: add project configuration TOML editor to the settings page
## 1.17.0
### Minor Changes
- 77fba27: fix: postgres version validation when activating ai in ai settings page
- ac6d1b6: feat: use name instead of awsName
## 1.16.3
### Patch Changes
- 87a37cf: fix: remove unnecessary isPlatform check from verify button disable logic on custom domains
- @nhost/react-apollo@12.0.2
- @nhost/nextjs@2.1.16
## 1.16.2
### Patch Changes
- a9413af: fix: update `GetAllWorkspacesAndProjects` query polling to use exponential backoff
- @nhost/react-apollo@12.0.1
- @nhost/nextjs@2.1.15
## 1.16.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.16.1",
"version": "1.23.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -21,17 +21,20 @@
"dependencies": {
"@apollo/client": "^3.9.9",
"@codemirror/lang-sql": "^6.6.2",
"@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "^6.4.0",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.4",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.5",
"@fontsource/inter": "^5.0.17",
"@fontsource/roboto-mono": "^5.0.17",
"@graphiql/react": "^0.20.3",
"@graphiql/react": "^0.22.3",
"@graphiql/toolkit": "^0.9.1",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^3.3.4",
"@iarna/toml": "^2.2.5",
"@mui/base": "5.0.0-beta.31",
"@mui/material": "^5.15.14",
"@mui/system": "^5.15.14",
@@ -45,6 +48,8 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.15.3",
"@tanstack/react-virtual": "^3.2.0",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/codemirror-theme-bbedit": "^4.22.2",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",
"analytics-node": "^6.2.0",
@@ -53,7 +58,7 @@
"date-fns": "^2.30.0",
"framer-motion": "^10.18.0",
"generate-password": "^1.7.1",
"graphiql": "^3.1.1",
"graphiql": "^3.3.1",
"graphql": "16.8.1",
"graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6",

View File

@@ -29,7 +29,7 @@ export default function CountrySelector({
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
className: 'z-[10000] w-[270px]',
},
}}
>

View File

@@ -1,4 +1,3 @@
import { ContactUs } from '@/components/common/ContactUs';
import { useDialog } from '@/components/common/DialogProvider';
import { NavLink } from '@/components/common/NavLink';
import { AccountMenu } from '@/components/layout/AccountMenu';
@@ -9,11 +8,9 @@ import { Logo } from '@/components/presentational/Logo';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Chip } from '@/components/ui/v2/Chip';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
import { DevAssistant } from '@/features/ai/DevAssistant';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { ApplicationStatus } from '@/types/application';
import { getToastStyleProps } from '@/utils/constants/settings';
@@ -38,8 +35,6 @@ export default function Header({ className, ...props }: HeaderProps) {
const { currentProject, refetch: refetchProject } =
useCurrentWorkspaceAndProject();
const isOwner = useIsCurrentUserOwner();
const isProjectUpdating =
currentProject?.appStates[0]?.stateId === ApplicationStatus.Updating;
@@ -105,25 +100,19 @@ export default function Header({ className, ...props }: HeaderProps) {
</Button>
{isPlatform && (
<Dropdown.Root>
<Dropdown.Trigger
hideChevron
className="rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
>
Contact us
</Dropdown.Trigger>
<Dropdown.Content
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<ContactUs
className="max-w-md"
isTeam={currentProject?.plan?.name === 'Team'}
isOwner={isOwner}
/>
</Dropdown.Content>
</Dropdown.Root>
<NavLink
underline="none"
href="/support"
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
sx={{
color: 'text.primary',
'&:hover': { backgroundColor: 'grey.200' },
}}
target="_blank"
rel="noopener noreferrer"
>
Support
</NavLink>
)}
<NavLink

View File

@@ -45,48 +45,53 @@ export default function SettingsLayout({
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
className="flex w-full flex-auto flex-col overflow-y-auto overflow-x-hidden"
>
<RetryableErrorBoundary>
<div className="flex flex-col space-y-2">
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row place-content-center gap-2"
>
<Text color="warning" className="text-sm ">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code
className={twMerge(
'rounded-md px-2 py-px',
theme.palette.mode === 'dark'
? 'bg-brown text-copper'
: 'bg-slate-200 text-slate-700',
)}
>
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository
and you only want these changes to apply to a subset of them,
please check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/cli/overlays"
>
docs.nhost.io/cli/overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
</div>
{children}
</RetryableErrorBoundary>
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex h-full flex-col"
>
<RetryableErrorBoundary>
<div className="flex flex-col space-y-2">
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row place-content-center gap-2"
>
<Text color="warning" className="text-sm ">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code
className={twMerge(
'rounded-md px-2 py-px',
theme.palette.mode === 'dark'
? 'bg-brown text-copper'
: 'bg-slate-200 text-slate-700',
)}
>
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository
and you only want these changes to apply to a subset of
them, please check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/cli/overlays"
>
docs.nhost.io/cli/overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
</div>
{children}
</RetryableErrorBoundary>
</Box>
</Box>
</ProjectLayout>
);

View File

@@ -3,6 +3,7 @@ import { Backdrop } from '@/components/ui/v2/Backdrop';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { SlidersIcon } from '@/components/ui/v2/icons/SlidersIcon';
import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
@@ -27,12 +28,17 @@ interface SettingsNavLinkProps extends ListItemButtonProps {
* @default true
*/
exact?: boolean;
/**
* Class name passed to the text element.
*/
textClassName?: string;
}
function SettingsNavLink({
exact = true,
href,
children,
textClassName,
...props
}: SettingsNavLinkProps) {
const router = useRouter();
@@ -52,7 +58,7 @@ function SettingsNavLink({
selected={active}
{...props}
>
<ListItem.Text>{children}</ListItem.Text>
<ListItem.Text className={textClassName}>{children}</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
);
@@ -114,13 +120,13 @@ export default function SettingsSidebar({
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
'absolute top-0 z-[35] flex h-full w-full flex-col justify-between overflow-auto border-r-1 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:pb-0 md:pt-2.5 md:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
className,
)}
{...props}
>
<nav aria-label="Settings navigation">
<nav aria-label="Settings navigation" className="px-2">
<List className="grid gap-2">
<SettingsNavLink
href="/general"
@@ -220,6 +226,20 @@ export default function SettingsSidebar({
</SettingsNavLink>
</List>
</nav>
<Box className="border-t">
<SettingsNavLink
href="/editor"
exact={false}
onClick={handleSelect}
className="flex w-full border group-focus-within:pr-9 group-hover:pr-9 group-active:pr-9"
textClassName="flex w-full justify-center"
>
<div className="flex w-full flex-row items-center justify-center space-x-4 py-2.5">
<SlidersIcon />
<span className="flex">Configuration Editor</span>
</div>
</SettingsNavLink>
</Box>
</Box>
<IconButton

View File

@@ -0,0 +1,132 @@
import { clsx } from 'clsx';
import {
forwardRef,
type ComponentPropsWithoutRef,
type ForwardedRef,
type ReactElement,
} from 'react';
import { Box } from '@/components/ui/v2/Box';
import { CopyToClipboardButton as CopyToClipboardButtonOriginal } from './CopyToClipboardButton';
import { getNodeText } from './getNodeText';
export interface CodeBlockPropsBase {
filename?: string;
/**
* Color of the filename text and the border underneath it when content is being shown.
*/
filenameColor?: string;
/**
* Text of the toast that appears when the code is copied to the clipboard.
*/
copyToClipboardToastTitle?: string;
}
export type CodeBlockProps = CodeBlockPropsBase &
Omit<ComponentPropsWithoutRef<'div'>, keyof CodeBlockPropsBase>;
/**
* Different from CodeGroup because we cannot use Headless UI's Tab component outside a Tab.Group
* Styling should look the same though.
*/
function CodeTabBar({
filename,
filenameColor,
children,
}: {
filename: string;
filenameColor?: string;
children?: ReactElement;
}) {
return (
<div className="flex text-xs leading-6 text-slate-400">
<div
className="flex flex-none items-center border-b border-t border-t-transparent px-4 py-1"
style={{ color: filenameColor, borderBottomColor: filenameColor }}
>
{filename}
</div>
<div className="bg-codeblock-tabs flex flex-auto items-center rounded-t border border-slate-500/30">
{children && (
<div className="flex flex-auto items-center justify-end space-x-4 px-4">
{children}
</div>
)}
</div>
</div>
);
}
interface CopyToClipboardButtonProps
extends Partial<
ComponentPropsWithoutRef<typeof CopyToClipboardButtonOriginal>
> {
filenameColor?: string;
tooltipColor?: string;
toastTitle?: string;
}
function CopyToClipboardButton({
tooltipColor,
filenameColor,
textToCopy,
toastTitle,
...props
}: CopyToClipboardButtonProps) {
return (
<CopyToClipboardButtonOriginal
textToCopy={textToCopy}
title={toastTitle}
{...props}
/>
);
}
export const CodeBlock = forwardRef(
(
{
filename,
filenameColor,
children,
className,
copyToClipboardToastTitle,
...props
}: CodeBlockProps,
ref: ForwardedRef<HTMLDivElement>,
) => (
<Box
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.200',
}}
className={clsx(
'not-prose relative mt-5 px-2',
filename && 'pt-2',
className,
)}
ref={ref}
{...props}
>
{filename ? (
<CodeTabBar filename={filename} filenameColor={filenameColor}>
<CopyToClipboardButton
filenameColor={filenameColor}
textToCopy={getNodeText(children)}
toastTitle={copyToClipboardToastTitle}
className="relative"
/>
</CodeTabBar>
) : (
<CopyToClipboardButton
filenameColor={filenameColor}
textToCopy={getNodeText(children)}
toastTitle={copyToClipboardToastTitle}
className="absolute right-3 top-0"
/>
)}
<pre className="overflow-x-auto">
<code className="font-mono">{children}</code>
</pre>
</Box>
),
);

View File

@@ -0,0 +1,55 @@
import { clsx } from 'clsx';
import { useEffect, useState } from 'react';
import {
IconButton,
type IconButtonProps,
} from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
export function CopyToClipboardButton({
textToCopy,
className,
title,
...props
}: {
textToCopy: string;
title: string;
} & IconButtonProps) {
const [disabled, setDisabled] = useState(true);
useEffect(() => {
// Hide copy button if the browser does not support it
if (typeof window !== 'undefined' && !navigator?.clipboard) {
console.error(
"The browser's Clipboard API is unavailable. The Clipboard API is only available on HTTPS.",
);
setDisabled(true);
} else {
setDisabled(false);
}
}, []);
// Hide copy button if you would copy an empty string
if (!textToCopy || disabled) {
return null;
}
return (
<IconButton
variant="borderless"
color="secondary"
className={clsx('group', className)}
onClick={(event) => {
event.stopPropagation();
copy(textToCopy, title);
}}
aria-label={textToCopy}
{...props}
>
<CopyIcon className="top-5 h-4 w-4" />
</IconButton>
);
}

View File

@@ -0,0 +1,17 @@
// Gets the text from a component as if you selected it with a mouse and copied it.
export const getNodeText = (node: any): string => {
if (['string', 'number'].includes(typeof node)) {
// Convert number into string
return node.toString();
}
if (node instanceof Array) {
return node.map(getNodeText).join('');
}
if (typeof node === 'object' && node?.props?.children) {
return getNodeText(node.props.children);
}
return '';
};

View File

@@ -0,0 +1,8 @@
import {
CodeBlock,
type CodeBlockProps,
type CodeBlockPropsBase,
} from './CodeBlock';
export type { CodeBlockPropsBase, CodeBlockProps };
export { CodeBlock };

View File

@@ -0,0 +1,28 @@
import { styled } from '@mui/material';
import type { AccordionProps as MaterialAccordionProps } from '@mui/material/Accordion';
import MaterialAccordion, { accordionClasses } from '@mui/material/Accordion';
export interface AccordionProps extends MaterialAccordionProps {}
const Accordion = styled(MaterialAccordion)<AccordionProps>(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
lineHeight: theme.typography.pxToRem(16),
borderBottom: `transparent solid 0px`,
boxShadow: `none`,
[`&.${accordionClasses.disabled}`]: {
backgroundColor: 'transparent',
},
[`&.${accordionClasses.root}`]: {
overflowX: 'hidden',
},
[`&.${accordionClasses.expanded}`]: {
marginTop: 0,
marginBottom: 0,
},
}));
Accordion.displayName = 'NhostAccordion';
export default Accordion;

View File

@@ -0,0 +1,18 @@
import { styled } from '@mui/material';
import type { AccordionActionsProps as MaterialAccordionActionsProps } from '@mui/material/AccordionActions';
import MaterialAccordionActions from '@mui/material/AccordionActions';
export interface AccordionActionsProps extends MaterialAccordionActionsProps {}
const AccordionActions = styled(
MaterialAccordionActions,
)<AccordionActionsProps>(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
lineHeight: theme.typography.pxToRem(16),
}));
AccordionActions.displayName = 'NhostAccordionActions';
export default AccordionActions;

View File

@@ -0,0 +1,21 @@
import { styled } from '@mui/material';
import type { AccordionDetailsProps as MaterialAccordionDetailsProps } from '@mui/material/AccordionDetails';
import MaterialAccordionDetails from '@mui/material/AccordionDetails';
export interface AccordionDetailsProps extends MaterialAccordionDetailsProps {}
const AccordionDetails = styled(
MaterialAccordionDetails,
)<AccordionDetailsProps>(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
lineHeight: theme.typography.pxToRem(16),
backgroundColor: theme.palette.grey[200],
marginTop: 0,
marginBottom: 0,
}));
AccordionDetails.displayName = 'NhostAccordionDetails';
export default AccordionDetails;

View File

@@ -0,0 +1,24 @@
import { styled } from '@mui/material';
import type { AccordionSummaryProps as MaterialAccordionSummaryProps } from '@mui/material/AccordionSummary';
import MaterialAccordionSummary, {
accordionSummaryClasses,
} from '@mui/material/AccordionSummary';
export interface AccordionSummaryProps extends MaterialAccordionSummaryProps {}
const AccordionSummary = styled(
MaterialAccordionSummary,
)<AccordionSummaryProps>(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
lineHeight: theme.typography.pxToRem(16),
[`&.${accordionSummaryClasses.disabled}`]: {
backgroundColor: 'transparent',
opacity: 1,
},
}));
AccordionSummary.displayName = 'NhostAccordionSummary';
export default AccordionSummary;

View File

@@ -0,0 +1,19 @@
import AccordionRoot from './Accordion';
import AccordionActions from './AccordionActions';
import AccordionDetails from './AccordionDetails';
import AccordionSummary from './AccordionSummary';
export { default as BaseAccordion } from './Accordion';
export * from './AccordionActions';
export { default as AccordionActions } from './AccordionActions';
export * from './AccordionDetails';
export { default as AccordionDetails } from './AccordionDetails';
export * from './AccordionSummary';
export { default as AccordionSummary } from './AccordionSummary';
export const Accordion = {
Root: AccordionRoot,
Details: AccordionDetails,
Summary: AccordionSummary,
Actions: AccordionActions,
};

View File

@@ -0,0 +1,47 @@
import { styled } from '@mui/material';
import type { BadgeProps as MaterialBadgeProps } from '@mui/material/Badge';
import MaterialBadge from '@mui/material/Badge';
import type { ElementType } from 'react';
export interface BadgeProps extends MaterialBadgeProps {
/**
* Custom component for the root node.
*/
component?: ElementType;
}
const Badge = styled(MaterialBadge)<BadgeProps>(({ theme }) => ({
fontFamily: theme.typography.fontFamily,
fontSize: theme.typography.pxToRem(12),
lineHeight: theme.typography.pxToRem(16),
fontWeight: 500,
padding: 0,
'& .MuiBadge-dot': {
minWidth: '0.625rem',
minHeight: '0.625rem',
borderRadius: '50%',
},
'& .MuiBadge-standard': {
padding: 0,
margin: 0,
minWidth: '0.625rem',
height: '0.625rem',
borderRadius: '50%',
},
'& .MuiBadge-colorError': {
backgroundColor: theme.palette.error.main,
},
'& .MuiBadge-colorWarning': {
backgroundColor: theme.palette.warning.main,
},
'& .MuiBadge-colorSuccess': {
backgroundColor: theme.palette.success.dark,
},
'& .MuiBadge-colorSecondary': {
backgroundColor: theme.palette.grey[500],
},
}));
Badge.displayName = 'NhostBadge';
export default Badge;

View File

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

View File

@@ -1,12 +1,12 @@
import { Backdrop } from '@/components/ui/v2/Backdrop';
import type { ButtonProps } from '@/components/ui/v2/Button';
import type { DialogTitleProps } from '@/components/ui/v2/Dialog';
import { styled } from '@mui/material';
import type { DialogProps as MaterialDialogProps } from '@mui/material/Dialog';
import MaterialDialog from '@mui/material/Dialog';
import type { DialogActionsProps } from '@mui/material/DialogActions';
import type { DialogContentProps } from '@mui/material/DialogContent';
import type { DialogContentTextProps } from '@mui/material/DialogContentText';
import type { DialogTitleProps } from '@mui/material/DialogTitle';
import Paper from '@mui/material/Paper';
import type { ReactNode } from 'react';

View File

@@ -0,0 +1,85 @@
import { render, screen } from '@/tests/testUtils';
import { test } from 'vitest';
import ErrorToast from './ErrorToast';
const oneMemberByWorkspaceError = {
name: 'ApolloError',
graphQLErrors: [
{
message: 'database query error',
extensions: {
path: '$.selectionSet.insertApp.args.object',
code: 'unexpected',
internal: {
arguments: [],
error: {
description: null,
exec_status: 'FatalError',
hint: null,
message:
'Only one workspace member is allowed for individual plans',
status_code: 'P0001',
},
prepared: false,
statement: '.....',
},
},
},
],
protocolErrors: [],
clientErrors: [],
networkError: null,
message: 'database query error',
};
const changeNodeInvalidVersionError = {
name: 'ApolloError',
graphQLErrors: [
{
message:
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
path: ['replaceConfigRawJSON'],
},
],
protocolErrors: [],
clientErrors: [],
networkError: null,
message:
'failed to resolve config: failed to validate config: config is not valid: #Config.functions.node.version: 2 errors in empty disjunction: (and 2 more errors)',
};
test('should render the error message when creating a project with an individual plan in a workspace with multiple users', () => {
const errorMessage =
'An error occurred while creating the project. Please try again.';
render(
<ErrorToast
isVisible
errorMessage={errorMessage}
error={oneMemberByWorkspaceError}
close={() => {}}
/>,
);
expect(
screen.getByText(
/Only one workspace member is allowed for individual plans/i,
),
).toBeInTheDocument();
});
test('should render the error message when changing the node version to an invalid value in configuration editor', () => {
const errorMessage =
'An error occurred while saving configuration. Please try again.';
render(
<ErrorToast
isVisible
errorMessage={errorMessage}
error={changeNodeInvalidVersionError}
close={() => {}}
/>,
);
const regex =
/failed to resolve config: failed to validate config: config is not valid: #Config\.functions\.node\.version: 2 errors in empty disjunction: \(and 2 more errors\)/i;
expect(screen.getByText(regex)).toBeInTheDocument();
});

View File

@@ -32,7 +32,7 @@ const getInternalErrorMessage = (
const graphqlError = error.graphQLErrors?.[0];
const graphqlExtensionsError = graphqlError?.extensions?.internal
?.error as { message: string };
return graphqlError.message || graphqlExtensionsError?.message || null;
return graphqlExtensionsError?.message || graphqlError.message || null;
}
if (error instanceof Error) {

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
export default function CommunityIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.5 10C7.29493 10 8.75 8.54493 8.75 6.75C8.75 4.95507 7.29493 3.5 5.5 3.5C3.70507 3.5 2.25 4.95507 2.25 6.75C2.25 8.54493 3.70507 10 5.5 10Z"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
<path
d="M9.71338 3.62107C10.1604 3.49513 10.6292 3.46644 11.0882 3.53693C11.5473 3.60743 11.9859 3.77547 12.3745 4.02975C12.7631 4.28403 13.0927 4.61863 13.3411 5.01102C13.5896 5.40342 13.751 5.84449 13.8146 6.30453C13.8782 6.76457 13.8425 7.2329 13.7098 7.67797C13.5772 8.12304 13.3507 8.53452 13.0457 8.8847C12.7406 9.23487 12.364 9.51561 11.9413 9.70799C11.5187 9.90038 11.0596 9.99996 10.5952 10"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M1 12.3373C1.50758 11.6153 2.18143 11.026 2.96466 10.6192C3.74788 10.2124 4.61748 10 5.50005 10C6.38262 9.99997 7.25224 10.2123 8.0355 10.619C8.81875 11.0258 9.49264 11.615 10.0003 12.337"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M10.5952 10C11.4779 9.99936 12.3477 10.2114 13.131 10.6182C13.9143 11.025 14.5881 11.6146 15.0952 12.337"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</svg>
);
}

View File

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

View File

@@ -0,0 +1,29 @@
import * as React from 'react';
export default function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={14}
height={14}
viewBox="0 0 14 14"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.40571 1.5H12.5943C13.3691 1.5 14 2.13086 14 2.91257V15.2143L12.5257 13.9114L11.696 13.1434L10.8183 12.3274L11.1817 13.596H3.40571C2.63086 13.596 2 12.9651 2 12.1834V2.91257C2 2.13086 2.63086 1.5 3.40571 1.5ZM9.49486 9.9C9.70057 10.1606 9.94743 10.4554 9.94743 10.4554C11.4629 10.4074 12.0457 9.41314 12.0457 9.41314C12.0457 7.20514 11.0583 5.41543 11.0583 5.41543C10.0709 4.67486 9.13143 4.69543 9.13143 4.69543L9.03543 4.80514C10.2011 5.16171 10.7429 5.676 10.7429 5.676C10.0297 5.28514 9.33029 5.09314 8.67886 5.01771C8.18514 4.96286 7.712 4.97657 7.29371 5.03143C7.2578 5.03143 7.2271 5.03665 7.19251 5.04254C7.18748 5.0434 7.18237 5.04427 7.17714 5.04514C6.93714 5.06571 6.35429 5.15486 5.62057 5.47714C5.36686 5.59371 5.216 5.676 5.216 5.676C5.216 5.676 5.78514 5.13429 7.01943 4.77771L6.95086 4.69543C6.95086 4.69543 6.01143 4.67486 5.024 5.41543C5.024 5.41543 4.03657 7.20514 4.03657 9.41314C4.03657 9.41314 4.61257 10.4074 6.128 10.4554C6.128 10.4554 6.38171 10.1469 6.58743 9.88628C5.71657 9.62571 5.38743 9.07714 5.38743 9.07714C5.38743 9.07714 5.456 9.12514 5.57943 9.19371C5.58629 9.20057 5.59314 9.20743 5.60686 9.21428C5.61714 9.22114 5.62743 9.22628 5.63771 9.23143C5.648 9.23657 5.65829 9.24171 5.66857 9.24857C5.84 9.34457 6.01143 9.42 6.16914 9.48171C6.45029 9.59143 6.78629 9.70114 7.17714 9.77657C7.69143 9.87257 8.29486 9.90686 8.95314 9.78343C9.27543 9.72857 9.60457 9.63257 9.94743 9.48857C10.1874 9.39943 10.4549 9.26914 10.736 9.084C10.736 9.084 10.3931 9.64628 9.49486 9.9ZM6.05942 8.01421C6.05942 7.59593 6.36799 7.25307 6.75885 7.25307C7.1497 7.25307 7.46513 7.59593 7.45827 8.01421C7.45827 8.4325 7.1497 8.77536 6.75885 8.77536C6.37485 8.77536 6.05942 8.4325 6.05942 8.01421ZM8.56227 8.01421C8.56227 7.59593 8.87084 7.25307 9.2617 7.25307C9.65256 7.25307 9.96113 7.59593 9.96113 8.01421C9.96113 8.4325 9.65256 8.77536 9.2617 8.77536C8.8777 8.77536 8.56227 8.4325 8.56227 8.01421Z"
fill="currentColor"
/>
</svg>
</svg>
);
}

View File

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

View File

@@ -0,0 +1,37 @@
import * as React from 'react';
export default function EnvelopeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={14}
height={14}
viewBox="0 0 14 14"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 3.5H14V12C14 12.1326 13.9473 12.2598 13.8536 12.3536C13.7598 12.4473 13.6326 12.5 13.5 12.5H2.5C2.36739 12.5 2.24021 12.4473 2.14645 12.3536C2.05268 12.2598 2 12.1326 2 12V3.5Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14 3.5L8 9L2 3.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</svg>
);
}

View File

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

View File

@@ -0,0 +1,33 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
function ExclamationFilledIcon(
props: IconProps,
ref: ForwardedRef<SVGSVGElement>,
) {
return (
<SvgIcon
width="7"
height="7"
viewBox="0 0 7 7"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Exclamation mark"
ref={ref}
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 3.5C7 5.433 5.433 7 3.5 7C1.567 7 0 5.433 0 3.5C0 1.567 1.567 0 3.5 0C5.433 0 7 1.567 7 3.5ZM3.96667 5.36667C3.96667 5.6244 3.75773 5.83333 3.5 5.83333C3.24227 5.83333 3.03333 5.6244 3.03333 5.36667C3.03333 5.10893 3.24227 4.9 3.5 4.9C3.75773 4.9 3.96667 5.10893 3.96667 5.36667ZM3.5 1.16667C3.20564 1.16667 2.97296 1.41615 2.99345 1.70979L3.16724 4.20075C3.17943 4.37554 3.32478 4.51111 3.5 4.51111C3.67522 4.51111 3.82057 4.37554 3.83276 4.20075L4.00655 1.70979C4.02704 1.41615 3.79436 1.16667 3.5 1.16667Z"
fill="currentColor"
/>
</SvgIcon>
);
}
ExclamationFilledIcon.displayName = 'NhostExclamationFilledIcon';
export default forwardRef(ExclamationFilledIcon);

View File

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

View File

@@ -0,0 +1,21 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function QuestionMarkIcon(props: IconProps) {
return (
<SvgIcon
width="320"
height="512"
aria-label="Question mark"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 320 512"
{...props}
>
<path d="M80 160c0-35.3 28.7-64 64-64h32c35.3 0 64 28.7 64 64v3.6c0 21.8-11.1 42.1-29.4 53.8l-42.2 27.1c-25.2 16.2-40.4 44.1-40.4 74V320c0 17.7 14.3 32 32 32s32-14.3 32-32v-1.4c0-8.2 4.2-15.8 11-20.2l42.2-27.1c36.6-23.6 58.8-64.1 58.8-107.7V160c0-70.7-57.3-128-128-128H144C73.3 32 16 89.3 16 160c0 17.7 14.3 32 32 32s32-14.3 32-32zm80 320a40 40 0 1 0 0-80 40 40 0 1 0 0 80z" />
</SvgIcon>
);
}
QuestionMarkIcon.displayName = 'NhostQuestionMarkIcon';
export default QuestionMarkIcon;

View File

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

View File

@@ -0,0 +1,63 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function ServicesOutlinedIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Services"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.8521 7.06294C15.8143 7.03192 15.485 6.77435 14.7986 6.73709C14.7878 6.7365 14.7769 6.73597 14.7659 6.73549C14.7223 6.7336 14.6772 6.7326 14.6307 6.7326C14.458 6.73338 14.2857 6.74604 14.1147 6.77049C14.0763 6.77598 14.0379 6.78208 13.9996 6.78876C13.9966 6.76748 13.9934 6.74642 13.9899 6.72556C13.9367 6.41145 13.8247 6.14509 13.6926 5.92522C13.5869 5.74941 13.4683 5.60332 13.3565 5.48631C13.1377 5.25722 12.9451 5.13955 12.9269 5.12843L12.7118 5L12.5703 5.21132C12.4532 5.39906 12.3569 5.59952 12.2832 5.80884C12.2455 5.916 12.2137 6.02549 12.188 6.13679C12.1785 6.17826 12.17 6.21957 12.1626 6.26069C12.0784 6.72436 12.1218 7.16514 12.2887 7.56346C12.3311 7.66471 12.3815 7.76323 12.4399 7.85868C12.0678 8.07332 11.471 8.12616 11.3503 8.13083H1.46954C1.21146 8.1312 1.00204 8.34712 1.00063 8.6143C0.989105 9.51047 1.13576 10.4013 1.43337 11.2429C1.77374 12.1671 2.28012 12.8478 2.93893 13.2644C3.67717 13.7325 4.87658 14 6.23616 14C6.85036 14.0019 7.4634 13.9444 8.06725 13.8281C8.90666 13.6685 9.71439 13.3648 10.457 12.9294C11.0689 12.5625 11.6196 12.0958 12.0879 11.5472C12.8644 10.6371 13.3296 9.62426 13.6755 8.72167C13.6783 8.7144 13.6811 8.70714 13.6839 8.69989H13.8221C14.6792 8.69989 15.2062 8.3448 15.4969 8.04724C15.69 7.85746 15.8408 7.62628 15.9386 7.36986L16 7.18397L15.8521 7.06294ZM11.9029 9.4592C11.6843 9.49512 11.4987 9.51062 11.3978 9.51453L11.374 9.51544H2.35702C2.41226 9.93468 2.51073 10.3477 2.65136 10.7474C2.90985 11.445 3.24967 11.8492 3.60646 12.0748L3.60778 12.0757C4.05853 12.3615 4.98755 12.6153 6.23616 12.6153H6.24016C6.77497 12.6171 7.30873 12.567 7.83441 12.4657L7.83733 12.4652C8.53499 12.3325 9.20535 12.0806 9.82092 11.7205C10.31 11.4265 10.7488 11.0538 11.1211 10.6177M11.9029 9.4592C11.6864 9.86254 11.4322 10.2531 11.1211 10.6177L11.9029 9.4592Z"
fill="currentColor"
/>
<path
d="M2.38514 7.54505H3.7092C3.77306 7.54505 3.8248 7.49328 3.8248 7.42944V6.25005C3.82516 6.18619 3.77369 6.13415 3.70985 6.13379C3.70964 6.13379 3.70941 6.13379 3.7092 6.13379H2.38514C2.32128 6.13379 2.26953 6.18556 2.26953 6.24939V6.25005V7.42942C2.26953 7.49328 2.32128 7.54505 2.38514 7.54505Z"
fill="currentColor"
/>
<path
d="M4.21003 7.54505H5.53409C5.59794 7.54505 5.64969 7.49328 5.64969 7.42944V6.25005C5.65005 6.18619 5.59857 6.13415 5.53472 6.13379C5.53451 6.13379 5.53427 6.13379 5.53407 6.13379H4.21001C4.14579 6.13379 4.09375 6.18583 4.09375 6.25005V7.42942C4.09413 7.49339 4.14606 7.54505 4.21003 7.54505Z"
fill="currentColor"
/>
<path
d="M6.06192 7.54505H7.38597C7.44983 7.54505 7.50157 7.49328 7.50157 7.42944V6.25005C7.50193 6.18619 7.45046 6.13415 7.3866 6.13379C7.38639 6.13379 7.38616 6.13379 7.38595 6.13379H6.06189C5.99804 6.13379 5.94629 6.18556 5.94629 6.24939V6.25005V7.42942C5.94631 7.49328 5.99808 7.54505 6.06192 7.54505Z"
fill="currentColor"
/>
<path
d="M7.89295 7.54505H9.21701C9.28097 7.54505 9.33291 7.49341 9.33326 7.42944V6.25005C9.33326 6.18583 9.28122 6.13379 9.21701 6.13379H7.89295C7.82909 6.13379 7.77734 6.18556 7.77734 6.24939V6.25005V7.42942C7.77734 7.49328 7.82911 7.54505 7.89295 7.54505Z"
fill="currentColor"
/>
<path
d="M4.21001 5.84874H5.53406C5.59801 5.84839 5.64967 5.79643 5.64967 5.73249V4.55311C5.64967 4.48925 5.5979 4.4375 5.53406 4.4375H4.21001C4.14606 4.4375 4.09411 4.48914 4.09375 4.55311V5.73249C4.09411 5.79656 4.14594 5.84839 4.21001 5.84874Z"
fill="currentColor"
/>
<path
d="M6.06189 5.84874H7.38595C7.4499 5.84839 7.50156 5.79643 7.50156 5.73249V4.55311C7.50156 4.48925 7.44979 4.4375 7.38595 4.4375H6.06189C5.99804 4.4375 5.94629 4.48927 5.94629 4.55311V5.73249C5.94629 5.79643 5.99795 5.84839 6.06189 5.84874Z"
fill="currentColor"
/>
<path
d="M7.89295 5.84874H9.21701C9.28108 5.84839 9.33291 5.79656 9.33326 5.73249V4.55311C9.33291 4.48914 9.28097 4.4375 9.21701 4.4375H7.89295C7.82909 4.4375 7.77734 4.48927 7.77734 4.55311V5.73249C7.77734 5.79643 7.82901 5.84839 7.89295 5.84874Z"
fill="currentColor"
/>
<path
d="M7.89295 4.1515H9.21701C9.28097 4.1515 9.33291 4.09983 9.33326 4.03589V2.85584C9.33291 2.79188 9.28097 2.74023 9.21701 2.74023H7.89295C7.82909 2.74023 7.77734 2.79198 7.77734 2.85584V4.03587C7.77734 4.09973 7.82911 4.1515 7.89295 4.1515Z"
fill="currentColor"
/>
<path
d="M9.73963 7.54505H11.0637C11.1277 7.54505 11.1796 7.49341 11.1799 7.42944V6.25005C11.1799 6.18583 11.1279 6.13379 11.0637 6.13379H9.73963C9.67579 6.13379 9.62402 6.18556 9.62402 6.24939V6.25005V7.42942C9.62402 7.49328 9.67579 7.54505 9.73963 7.54505Z"
fill="currentColor"
/>
</SvgIcon>
);
}
ServicesOutlinedIcon.displayName = 'NhostServicesIcon';
export default ServicesOutlinedIcon;

View File

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

View File

@@ -0,0 +1,34 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function SlidersIcon(props: IconProps) {
return (
<SvgIcon
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg"
aria-label="Sliders"
{...props}
>
<line x1="21" x2="14" y1="4" y2="4" />
<line x1="10" x2="3" y1="4" y2="4" />
<line x1="21" x2="12" y1="12" y2="12" />
<line x1="8" x2="3" y1="12" y2="12" />
<line x1="21" x2="16" y1="20" y2="20" />
<line x1="12" x2="3" y1="20" y2="20" />
<line x1="14" x2="14" y1="2" y2="6" />
<line x1="8" x2="8" y1="10" y2="14" />
<line x1="16" x2="16" y1="18" y2="22" />
</SvgIcon>
);
}
SlidersIcon.displayName = 'NhostSlidersIcon';
export default SlidersIcon;

View File

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

View File

@@ -117,7 +117,7 @@ export default function ArgumentsFormSection({
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
className: 'z-[10000] w-[270px]',
},
}}
>

View File

@@ -32,8 +32,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
import { DisableAIServiceConfirmationDialog } from './DisableAIServiceConfirmationDialog';
const MIN_POSTGRES_VERSION_SUPPORTING_AI = '14.6-20231018-1';
import { isPostgresVersionValidForAI } from '@/features/ai/settings/utils/isPostgresVersionValidForAI';
const validationSchema = Yup.object({
version: Yup.object({
@@ -165,7 +164,7 @@ export default function AISettings() {
]);
const toggleAIService = async (enabled: boolean) => {
if (postgresVersion < MIN_POSTGRES_VERSION_SUPPORTING_AI) {
if (!isPostgresVersionValidForAI(postgresVersion)) {
toast.error(
'In order to enable the AI service you need to update your database version to 14.6-20231018-1 or newer.',
{
@@ -495,3 +494,4 @@ export default function AISettings() {
</Box>
);
}

View File

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

View File

@@ -0,0 +1,22 @@
import { test, vi } from 'vitest';
import isPostgresVersionValidForAI from './isPostgresVersionValidForAI';
beforeEach(() => {
vi.resetModules();
});
test('greater than minimum version, minor version with two digits, should be valid', () => {
const postgresVersion = '14.11-20240515-1';
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(true);
});
test('less than minimum version, should be invalid', () => {
const postgresVersion = '14.6-20221110-1';
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(false);
});
test('equal to minimum version, should be valid', () => {
const postgresVersion = '14.6-20231018-1';
expect(isPostgresVersionValidForAI(postgresVersion)).toBe(true);
});

View File

@@ -0,0 +1,18 @@
/**
* Check if the given postgres version is valid for enabling AI in the project
*
* @param postgresVersion - Postgres version used in the project.
* @returns Whether is valid for enabling AI.
*/
export default function isPostgresVersionValidForAI(
postgresVersion: string,
): boolean {
const MIN_POSTGRES_VERSION_SUPPORTING_AI = '14.6-20231018-1';
if (/^14\.6-/.test(postgresVersion)) {
return postgresVersion >= MIN_POSTGRES_VERSION_SUPPORTING_AI;
}
// Note: No need to account for versions less than 14.6
return true;
}

View File

@@ -5,7 +5,9 @@ import { useGetAnnouncementsQuery } from '@/utils/__generated__/graphql';
import formatDistance from 'date-fns/formatDistance';
export default function Announcements() {
const { data, loading, error } = useGetAnnouncementsQuery();
const { data, loading, error } = useGetAnnouncementsQuery({
fetchPolicy: 'cache-first',
});
const announcements = data?.announcements || [];

View File

@@ -7,6 +7,7 @@ import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { OverviewDeployments } from '@/features/projects/overview/components/OverviewDeployments';
import { OverviewDocumentation } from '@/features/projects/overview/components/OverviewDocumentation';
import { OverviewMetrics } from '@/features/projects/overview/components/OverviewMetrics';
import { OverviewProjectHealth } from '@/features/projects/overview/components/OverviewProjectHealth';
import { OverviewProjectInfo } from '@/features/projects/overview/components/OverviewProjectInfo';
import { OverviewRepository } from '@/features/projects/overview/components/OverviewRepository';
import { OverviewTopBar } from '@/features/projects/overview/components/OverviewTopBar';
@@ -92,6 +93,8 @@ export default function ApplicationLive({
</div>
<div className="grid grid-flow-row content-start gap-8 lg:col-span-1 lg:gap-12">
<OverviewProjectHealth />
<Divider />
<OverviewProjectInfo />
<Divider />
<OverviewRepository />

View File

@@ -0,0 +1,192 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getToastStyleProps } from '@/utils/constants/settings';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
useGetConfigRawJsonQuery,
useReplaceConfigRawJsonMutation,
} from '@/utils/__generated__/graphql';
import { StreamLanguage } from '@codemirror/language';
import { toml } from '@codemirror/legacy-modes/mode/toml';
import * as TOML from '@iarna/toml';
import { useTheme } from '@mui/material';
import { bbedit } from '@uiw/codemirror-theme-bbedit';
import { githubDark } from '@uiw/codemirror-theme-github';
import CodeMirror from '@uiw/react-codemirror';
import { useCallback, useEffect, useState } from 'react';
import toast from 'react-hot-toast';
export default function TOMLEditor() {
const theme = useTheme();
const isPlatform = useIsPlatform();
const [tomlCode, setTOMLCode] = useState('');
const [previousTOMLCode, setPreviousTOMLCode] = useState(''); // used to revert changes
const [isDirty, setIsDirty] = useState(false);
const [isSaving, setIsSaving] = useState(false); // used to show loading spinner on save
const { openDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
// fetch the initial TOML code from the server
const { data, loading } = useGetConfigRawJsonQuery({
variables: {
appID: currentProject?.id,
},
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),
});
const [saveConfigMutation] = useReplaceConfigRawJsonMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const removeTOMLIndentation = (tomlStr: string) => {
const trimmedLines = tomlStr.split('\n').map((line) => line.trimStart());
return trimmedLines.join('\n');
};
useEffect(() => {
// Load TOML code from the server on initial load
if (!loading && data) {
const jsonData = JSON.parse(data?.configRawJSON);
const tomlStr = TOML.stringify(jsonData);
const unindentedTOMLConfig = removeTOMLIndentation(tomlStr);
setTOMLCode(unindentedTOMLConfig);
setPreviousTOMLCode(unindentedTOMLConfig);
}
}, [loading, data]);
const onChange = useCallback((value: string) => {
setTOMLCode(value);
setIsDirty(true);
}, []);
const handleRevert = () => {
setTOMLCode(previousTOMLCode);
setIsDirty(false);
};
const handleSave = async () => {
setIsSaving(true);
let jsonEditedConfig;
try {
jsonEditedConfig = TOML.parse(tomlCode);
} catch (error) {
const toastStyle = getToastStyleProps();
const { line, col } = error;
let message = `An error occurred while parsing the TOML file. Please check the syntax.`;
if (line !== undefined && col !== undefined) {
message = `An error occurred while parsing the TOML file. Please check the syntax at line ${line}, column ${col}.`;
}
toast.error(message, {
style: toastStyle.style,
...toastStyle.error,
});
setIsSaving(false);
return;
}
const rawJSONString = JSON.stringify(jsonEditedConfig);
await execPromiseWithErrorToast(
async () => {
const {
data: { replaceConfigRawJSON: updatedConfig },
} = await saveConfigMutation({
variables: {
appID: currentProject?.id,
rawJSON: rawJSONString,
},
});
if (updatedConfig) {
const jsonUpdatedConfig = JSON.parse(updatedConfig);
const updatedTOMLConfig = TOML.stringify(jsonUpdatedConfig);
const unindentedTOMLConfig = removeTOMLIndentation(updatedTOMLConfig);
setTOMLCode(unindentedTOMLConfig);
setPreviousTOMLCode(unindentedTOMLConfig);
}
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
setIsDirty(false);
setIsSaving(false);
},
{
loadingMessage: 'Saving configuration...',
successMessage: 'Configuration has been saved successfully.',
errorMessage:
'An error occurred while saving configuration. Please try again.',
onError: () => {
setIsSaving(false);
},
},
);
};
return (
<>
<Box className="flex w-full flex-col space-y-2 border-b p-4">
<Text className="font-semibold">Configuration Editor</Text>
</Box>
<Box className="h-full overflow-auto">
{loading ? (
<Box
className="h-full w-full animate-pulse"
sx={{ backgroundColor: 'grey.200' }}
/>
) : (
<CodeMirror
value={tomlCode}
height="100%"
width="100%"
theme={theme.palette.mode === 'light' ? bbedit : githubDark}
basicSetup={{
searchKeymap: false,
}}
extensions={[StreamLanguage.define(toml)]}
onChange={onChange}
/>
)}
</Box>
<Box className="grid w-full grid-flow-col justify-end gap-3 place-self-end border-t-1 px-4 py-3 md:justify-between">
<Button
variant="outlined"
disabled={loading || !isDirty}
onClick={handleRevert}
color="secondary"
>
Revert changes
</Button>
<Button
type="submit"
disabled={loading || !isDirty}
loading={isSaving}
className="justify-self-end"
onClick={handleSave}
>
Save
</Button>
</Box>
</>
);
}

View File

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

View File

@@ -108,7 +108,7 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
id: null,
countryCode: null,
city: null,
awsName: null,
name: null,
domain: null,
},
createdAt: new Date().toISOString(),

View File

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

View File

@@ -0,0 +1,72 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type { ServiceHealthInfo } from '@/features/projects/overview/health';
import {
useGetProjectServicesHealthQuery,
type GetProjectServicesHealthQuery,
type GetProjectServicesHealthQueryVariables,
} from '@/utils/__generated__/graphql';
import type { QueryHookOptions } from '@apollo/client';
import { useVisibilityChange } from '@uidotdev/usehooks';
import { useEffect } from 'react';
export interface UseServiceStatusOptions
extends QueryHookOptions<
GetProjectServicesHealthQuery,
GetProjectServicesHealthQueryVariables
> {
shouldPoll?: boolean;
}
export default function useServiceStatus(
options: UseServiceStatusOptions = {},
) {
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const isVisible = useVisibilityChange();
const { data, loading, refetch, startPolling, stopPolling } =
useGetProjectServicesHealthQuery({
...options,
variables: { ...options.variables, appId: currentProject?.id },
skip: !isPlatform || !currentProject,
skipPollAttempt: () => !isVisible,
});
// Fetch when mounted
useEffect(() => {
refetch();
}, [refetch]);
useEffect(() => {
if (options.shouldPoll) {
startPolling(options.pollInterval || 10000);
}
return () => stopPolling();
}, [stopPolling, startPolling, options.shouldPoll, options.pollInterval]);
const serviceMap: { [key: string]: ServiceHealthInfo | undefined } = {};
data?.getProjectStatus?.services.forEach((service) => {
serviceMap[service.name] = service;
});
const {
'hasura-auth': auth,
'hasura-storage': storage,
postgres,
hasura,
ai,
...run
} = serviceMap;
return {
loading,
auth,
storage,
postgres,
hasura,
ai,
run,
};
}

View File

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

View File

@@ -0,0 +1,156 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
Software_Type_Enum,
useGetConfiguredVersionsQuery,
useGetRecommendedSoftwareVersionsQuery,
type GetConfiguredVersionsQuery,
type GetConfiguredVersionsQueryVariables,
} from '@/utils/__generated__/graphql';
import type { QueryHookOptions } from '@apollo/client';
import { useVisibilityChange } from '@uidotdev/usehooks';
import { useEffect } from 'react';
export interface UseSoftwareVersionsInfoOptions
extends QueryHookOptions<
GetConfiguredVersionsQuery,
GetConfiguredVersionsQueryVariables
> {}
type ServiceVersionInfo = {
configuredVersion: string | undefined;
recommendedVersions: string[];
isVersionMismatch: boolean;
};
export default function useSoftwareVersionsInfo(
options: UseSoftwareVersionsInfoOptions = {},
): {
loading: boolean;
auth: ServiceVersionInfo;
storage: ServiceVersionInfo;
postgres: ServiceVersionInfo;
hasura: ServiceVersionInfo;
ai: ServiceVersionInfo;
isAIEnabled: boolean;
} {
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const isVisible = useVisibilityChange();
// Recommended software versions are not polled by default
const { data: recommendedVersionsData, loading: loadingRecommendedVersions } =
useGetRecommendedSoftwareVersionsQuery({
skip: !isPlatform || !currentProject,
});
const {
data: configuredVersionsData,
loading: loadingConfiguredVersions,
refetch: refetchConfiguredVersions,
stopPolling,
} = useGetConfiguredVersionsQuery({
...options,
variables: { ...options.variables, appId: currentProject?.id },
skip: !isPlatform || !currentProject,
skipPollAttempt: () => !isVisible,
pollInterval: options.pollInterval || 10000,
});
// fetch when mounted
useEffect(() => {
refetchConfiguredVersions();
return () => stopPolling();
}, [refetchConfiguredVersions, stopPolling]);
const recommendedVersions = {
'hasura-auth': [],
'hasura-storage': [],
postgres: [],
hasura: [],
ai: [],
};
recommendedVersionsData?.softwareVersions.forEach(({ software, version }) => {
switch (software) {
case Software_Type_Enum.Auth:
recommendedVersions['hasura-auth'].push(version);
break;
case Software_Type_Enum.Storage:
recommendedVersions['hasura-storage'].push(version);
break;
case Software_Type_Enum.PostgreSql:
recommendedVersions.postgres.push(version);
break;
case Software_Type_Enum.Hasura:
recommendedVersions.hasura.push(version);
break;
case Software_Type_Enum.Graphite:
recommendedVersions.ai.push(version);
break;
default:
break;
}
});
const isVersionMismatch = (
service: string,
configuredVersion: string | undefined,
) =>
!recommendedVersions[service].some(
(version) => version === configuredVersion,
);
// Check if configured version can't be found in recommended versions
const isAuthVersionMismatch = isVersionMismatch(
'hasura-auth',
configuredVersionsData?.config?.auth?.version,
);
const isStorageVersionMismatch = isVersionMismatch(
'hasura-storage',
configuredVersionsData?.config?.storage?.version,
);
const isPostgresVersionMismatch = isVersionMismatch(
'postgres',
configuredVersionsData?.config?.postgres?.version,
);
const isHasuraVersionMismatch = isVersionMismatch(
'hasura',
configuredVersionsData?.config?.hasura?.version,
);
const isAIVersionMismatch = isVersionMismatch(
'ai',
configuredVersionsData?.config?.ai?.version,
);
return {
loading: loadingConfiguredVersions || loadingRecommendedVersions,
auth: {
configuredVersion: configuredVersionsData?.config?.auth?.version,
recommendedVersions: recommendedVersions['hasura-auth'],
isVersionMismatch: isAuthVersionMismatch,
},
storage: {
configuredVersion: configuredVersionsData?.config?.storage?.version,
recommendedVersions: recommendedVersions['hasura-storage'],
isVersionMismatch: isStorageVersionMismatch,
},
postgres: {
configuredVersion: configuredVersionsData?.config?.postgres?.version,
recommendedVersions: recommendedVersions.postgres,
isVersionMismatch: isPostgresVersionMismatch,
},
hasura: {
configuredVersion: configuredVersionsData?.config?.hasura?.version,
recommendedVersions: recommendedVersions.hasura,
isVersionMismatch: isHasuraVersionMismatch,
},
ai: {
configuredVersion: configuredVersionsData?.config?.ai?.version,
recommendedVersions: recommendedVersions.ai,
isVersionMismatch: isAIVersionMismatch,
},
isAIEnabled: Boolean(configuredVersionsData?.config?.ai),
};
}

View File

@@ -31,7 +31,7 @@ afterEach(() => {
const region: ProjectFragment['region'] = {
id: '1',
awsName: 'eu-west-1',
name: 'eu-west-1',
domain: 'nhost.run',
city: 'Dublin',
countryCode: 'IE',

View File

@@ -89,7 +89,7 @@ export default function generateAppServiceUrl(
const constructedDomain = [
subdomain,
service,
region?.awsName,
region?.name,
region?.domain || 'nhost.run',
]
.filter(Boolean)

View File

@@ -165,7 +165,7 @@ export default function AuthDomain() {
<VerifyDomain
recordType="CNAME"
hostname={auth_fqdn}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>

View File

@@ -167,7 +167,7 @@ export default function HasuraDomain() {
<VerifyDomain
recordType="CNAME"
hostname={hasura_fqdn}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>

View File

@@ -162,7 +162,7 @@ export default function RunServicePortDomain({
<VerifyDomain
recordType="CNAME"
hostname={runServicePortFQDN}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>

View File

@@ -169,7 +169,7 @@ export default function ServerlessFunctionsDomain() {
<VerifyDomain
recordType="CNAME"
hostname={functions_fqdn}
value={`lb.${currentProject.region.awsName}.${currentProject.region.domain}.`}
value={`lb.${currentProject.region.name}.${currentProject.region.domain}.`}
onHostNameVerified={() => setIsVerified(true)}
/>
</div>

View File

@@ -131,7 +131,7 @@ export default function VerifyDomain({
</div>
{isPlatform ? (
<Button
disabled={loading || !hostname || isPlatform}
disabled={loading || !hostname}
onClick={handleVerifyDomain}
className="mt-4 sm:absolute sm:bottom-0 sm:right-0 sm:mt-0"
>

View File

@@ -101,7 +101,7 @@ export default function SystemEnvironmentVariableSettings() {
const systemEnvironmentVariables = [
{ key: 'NHOST_SUBDOMAIN', value: currentProject.subdomain },
{ key: 'NHOST_REGION', value: currentProject.region.awsName },
{ key: 'NHOST_REGION', value: currentProject.region.name },
{
key: 'NHOST_HASURA_URL',
value:

View File

@@ -0,0 +1,67 @@
import { Box } from '@/components/ui/v2/Box';
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
import { QuestionMarkIcon } from '@/components/ui/v2/icons/QuestionMarkIcon';
import { serviceStateToThemeColor } from '@/features/projects/overview/health';
import { ServiceState } from '@/utils/__generated__/graphql';
interface AccordionHealthBadgeProps {
serviceState?: ServiceState;
unknownState?: boolean;
/*
* Blinking animation to indicate that the service is updating.
*/
blink?: boolean;
}
export default function AccordionHealthBadge({
serviceState,
unknownState,
blink,
}: AccordionHealthBadgeProps) {
if (unknownState) {
return (
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(serviceState),
}}
className="flex h-2.5 w-2.5 items-center justify-center rounded-full"
>
<QuestionMarkIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-3/4 w-3/4 stroke-2"
/>
</Box>
);
}
if (serviceState === ServiceState.Running) {
return (
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(serviceState),
}}
className="flex h-2.5 w-2.5 items-center justify-center rounded-full"
>
<CheckIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-3/4 w-3/4 stroke-2"
/>
</Box>
);
}
return (
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(serviceState),
}}
className={`h-2.5 w-2.5 rounded-full ${blink ? 'animate-pulse' : ''}`}
/>
);
}

View File

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

View File

@@ -0,0 +1,213 @@
import { useDialog } from '@/components/common/DialogProvider';
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
import { ServicesOutlinedIcon } from '@/components/ui/v2/icons/ServicesOutlinedIcon';
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
import { useSoftwareVersionsInfo } from '@/features/projects/common/hooks/useSoftwareVersionsInfo';
import { OverviewProjectHealthModal } from '@/features/projects/overview/components/OverviewProjectHealthModal';
import { ProjectHealthCard } from '@/features/projects/overview/components/ProjectHealthCard';
import { RunStatusTooltip } from '@/features/projects/overview/components/RunStatusTooltip';
import { ServiceVersionTooltip } from '@/features/projects/overview/components/ServiceVersionTooltip';
import {
baseServices,
findHighestImportanceState,
} from '@/features/projects/overview/health';
export default function OverviewProjectHealth() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { openDialog, closeDialog } = useDialog();
const {
loading: loadingVersions,
auth: authVersionInfo,
storage: storageVersionInfo,
postgres: postgresVersionInfo,
hasura: hasuraVersionInfo,
ai: aiVersionInfo,
isAIEnabled,
} = useSoftwareVersionsInfo();
const {
loading: loadingProjectServicesHealth,
auth: authStatus,
storage: storageStatus,
postgres: postgresStatus,
hasura: hasuraStatus,
ai: aiStatus,
run: runStatus,
} = useServiceStatus({
shouldPoll: true,
});
if (loadingVersions || loadingProjectServicesHealth) {
return (
<div className="grid grid-flow-row content-start gap-6">
<Text variant="h3">Project Health</Text>
<div className="flex flex-row flex-wrap items-center justify-start gap-2 lg:gap-2">
<ProjectHealthCard
isLoading
icon={<UserIcon className="m-1 h-6 w-6" />}
/>
<ProjectHealthCard
isLoading
icon={<DatabaseIcon className="m-1 h-6 w-6" />}
/>
<ProjectHealthCard
isLoading
icon={<StorageIcon className="m-1 h-6 w-6" />}
/>
<ProjectHealthCard
isLoading
icon={<HasuraIcon className="m-1 h-6 w-6" />}
/>
</div>
</div>
);
}
const openHealthModal = async (
defaultExpanded: keyof typeof baseServices | 'run',
) => {
openDialog({
component: (
<OverviewProjectHealthModal defaultExpanded={defaultExpanded} />
),
props: {
PaperProps: { className: 'p-0 max-w-2xl w-full' },
titleProps: {
onClose: closeDialog,
},
},
title: 'Service State',
});
};
const authTooltipElem = (
<ServiceVersionTooltip
serviceName={baseServices['hasura-auth'].displayName}
serviceKey="hasura-auth"
usedVersion={authVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={authVersionInfo?.isVersionMismatch}
recommendedVersions={authVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={authStatus?.state}
/>
);
const hasuraTooltipElem = (
<ServiceVersionTooltip
serviceName={baseServices.hasura.displayName}
serviceKey="hasura"
usedVersion={hasuraVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
recommendedVersions={hasuraVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={hasuraStatus?.state}
/>
);
const postgresTooltipElem = (
<ServiceVersionTooltip
serviceName={baseServices.postgres.displayName}
serviceKey="postgres"
usedVersion={postgresVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={postgresVersionInfo?.isVersionMismatch}
recommendedVersions={postgresVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={postgresStatus?.state}
/>
);
const storageTooltipElem = (
<ServiceVersionTooltip
serviceName={baseServices['hasura-storage'].displayName}
serviceKey="hasura-storage"
usedVersion={storageVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={storageVersionInfo?.isVersionMismatch}
recommendedVersions={storageVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={storageStatus?.state}
/>
);
const aiTooltipElem = (
<ServiceVersionTooltip
serviceName={baseServices.ai.displayName}
serviceKey="ai"
usedVersion={aiVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={aiVersionInfo?.isVersionMismatch}
recommendedVersions={aiVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={aiStatus?.state}
/>
);
const runServices = Object.values(runStatus).filter((service) =>
service?.name?.startsWith('run-'),
);
const runServicesStates = runServices.map((service) => service.state);
const runServicesState = findHighestImportanceState(runServicesStates);
return (
<div className="grid grid-flow-row content-start gap-6">
<Text variant="h3">Project Health</Text>
{currentProject && (
<div className="flex flex-row flex-wrap items-center justify-start gap-2 lg:gap-2">
<ProjectHealthCard
icon={<UserIcon className="m-1 h-6 w-6" />}
tooltip={authTooltipElem}
isVersionMismatch={authVersionInfo?.isVersionMismatch}
state={authStatus?.state}
/>
<ProjectHealthCard
icon={<DatabaseIcon className="m-1 h-6 w-6" />}
tooltip={postgresTooltipElem}
isVersionMismatch={postgresVersionInfo?.isVersionMismatch}
state={postgresStatus?.state}
/>
<ProjectHealthCard
icon={<StorageIcon className="m-1 h-6 w-6" />}
tooltip={storageTooltipElem}
isVersionMismatch={storageVersionInfo?.isVersionMismatch}
state={storageStatus?.state}
/>
<ProjectHealthCard
icon={<HasuraIcon className="m-1 h-6 w-6" />}
tooltip={hasuraTooltipElem}
isVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
state={hasuraStatus?.state}
/>
{isAIEnabled && (
<ProjectHealthCard
icon={<AIIcon className="m-1 h-6 w-6" />}
tooltip={aiTooltipElem}
isVersionMismatch={aiVersionInfo?.isVersionMismatch}
state={aiStatus?.state}
/>
)}
{Object.values(runServices).length > 0 && (
<ProjectHealthCard
icon={<ServicesOutlinedIcon className="m-1 h-6 w-6" />}
tooltip={
<RunStatusTooltip
servicesStatusInfo={Object.values(runServices)}
openHealthModal={openHealthModal}
/>
}
state={runServicesState}
/>
)}
</div>
)}
</div>
);
}

View File

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

View File

@@ -0,0 +1,132 @@
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
import { ServicesOutlinedIcon } from '@/components/ui/v2/icons/ServicesOutlinedIcon';
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
import { ServiceAccordion } from '@/features/projects/overview/components/ServiceAccordion';
import {
findHighestImportanceState,
type baseServices,
} from '@/features/projects/overview/health';
import { removeTypename } from '@/utils/helpers';
import { twMerge } from 'tailwind-merge';
export interface OverviewProjectHealthModalProps {
defaultExpanded?: keyof typeof baseServices | 'run';
}
export default function OverviewProjectHealthModal({
defaultExpanded,
}: OverviewProjectHealthModalProps) {
const { auth, storage, postgres, hasura, ai, run } = useServiceStatus({
fetchPolicy: 'cache-only',
shouldPoll: false,
});
const runServices = Object.values(run).filter((service) =>
service.name.startsWith('run-'),
);
const isAuthExpandedByDefault = defaultExpanded === 'hasura-auth';
const isPostgresExpandedByDefault = defaultExpanded === 'postgres';
const isStorageExpandedByDefault = defaultExpanded === 'hasura-storage';
const isHasuraExpandedByDefault = defaultExpanded === 'hasura';
const isAIExpandedByDefault = defaultExpanded === 'ai';
const isRunExpandedByDefault = defaultExpanded === 'run';
const getServiceInfo = (service) => {
const info = removeTypename(service);
return JSON.stringify(info, null, 2);
};
const serviceInfo = {
auth: getServiceInfo(auth),
storage: getServiceInfo(storage),
postgres: getServiceInfo(postgres),
hasura: getServiceInfo(hasura),
ai: getServiceInfo(ai),
run: getServiceInfo(Object.values(runServices)),
};
const runServicesState = findHighestImportanceState(
Object.values(runServices).map((service) => service.state),
);
return (
<Box className={twMerge('w-full rounded-lg pt-2 text-left')}>
<Box
sx={{
borderColor: 'text.dark',
}}
className="grid grid-flow-row"
>
<Divider />
<ServiceAccordion
icon={<UserIcon className="h-4 w-4" />}
serviceName="Auth"
serviceInfo={serviceInfo.auth}
replicaCount={auth?.replicas?.length}
serviceState={auth?.state}
defaultExpanded={isAuthExpandedByDefault}
/>
<Divider />
<ServiceAccordion
icon={<DatabaseIcon className="h-4 w-4" />}
serviceName="Postgres"
serviceInfo={serviceInfo.postgres}
replicaCount={postgres?.replicas?.length}
serviceState={postgres?.state}
defaultExpanded={isPostgresExpandedByDefault}
/>
<Divider />
<ServiceAccordion
icon={<StorageIcon className="h-4 w-4" />}
serviceName="Storage"
serviceInfo={serviceInfo.storage}
replicaCount={storage?.replicas?.length}
serviceState={storage?.state}
defaultExpanded={isStorageExpandedByDefault}
/>
<Divider />
<ServiceAccordion
icon={<HasuraIcon className="h-4 w-4" />}
serviceName="Hasura"
serviceInfo={serviceInfo.hasura}
replicaCount={hasura?.replicas?.length}
serviceState={hasura?.state}
defaultExpanded={isHasuraExpandedByDefault}
/>
{ai ? (
<>
<Divider />
<ServiceAccordion
icon={<AIIcon className="h-4 w-4" />}
serviceName="AI"
serviceInfo={serviceInfo.ai}
replicaCount={ai?.replicas?.length}
serviceState={ai?.state}
defaultExpanded={isAIExpandedByDefault}
/>
</>
) : null}
{runServices && Object.values(runServices).length > 0 ? (
<>
<Divider />
<ServiceAccordion
icon={<ServicesOutlinedIcon className="h-4 w-4" />}
serviceName="Run"
serviceInfo={serviceInfo.run}
replicaCount={0}
serviceState={runServicesState}
defaultExpanded={isRunExpandedByDefault}
/>
</>
) : null}
</Box>
</Box>
);
}

View File

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

View File

@@ -7,7 +7,7 @@ export default function OverviewProjectInfo() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { region, subdomain } = currentProject || {};
const isRegionAvailable =
region?.awsName && region?.countryCode && region?.city;
region?.name && region?.countryCode && region?.city;
return (
<div className="grid grid-flow-row content-start gap-6">
@@ -17,7 +17,7 @@ export default function OverviewProjectInfo() {
<div className="grid grid-flow-row gap-3">
<InfoCard
title="Region"
value={region?.awsName}
value={region?.name}
customValue={
region?.countryCode &&
region?.city && (
@@ -30,7 +30,7 @@ export default function OverviewProjectInfo() {
/>
<Text className="truncate text-sm font-medium">
{region.city} ({region.awsName})
{region.city} ({region.name})
</Text>
</div>
)

View File

@@ -0,0 +1,107 @@
import { Badge, type BadgeProps } from '@/components/ui/v2/Badge';
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
import { ExclamationFilledIcon } from '@/components/ui/v2/icons/ExclamationFilledIcon';
import { QuestionMarkIcon } from '@/components/ui/v2/icons/QuestionMarkIcon';
export interface ProjectHealthBadgeProps extends BadgeProps {
badgeVariant?: 'standard' | 'dot';
badgeColor?: 'success' | 'error' | 'warning' | 'secondary';
unknownState?: boolean;
showExclamation?: boolean;
showCheckIcon?: boolean;
isLoading?: boolean;
blink?: boolean;
}
export default function ProjectHealthBadge({
badgeColor,
badgeVariant,
showExclamation,
showCheckIcon,
unknownState,
blink,
children,
...props
}: ProjectHealthBadgeProps) {
let innerBadgeContent = null;
if (unknownState) {
innerBadgeContent = (
<QuestionMarkIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-2 w-2 stroke-2"
/>
);
} else if (showCheckIcon) {
innerBadgeContent = (
<CheckIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-2 w-2 stroke-2"
/>
);
}
if (!badgeColor) {
return <div>{children}</div>;
}
if (showExclamation) {
return (
<Badge
variant="standard"
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
badgeContent={
<ExclamationFilledIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.600',
}}
className="h-2.5 w-2.5"
/>
}
>
<Badge
color={badgeColor}
variant={badgeVariant}
badgeContent={innerBadgeContent}
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'text.primary',
}}
componentsProps={{
badge: {
className: blink ? 'animate-pulse' : '',
},
}}
{...props}
>
{children}
</Badge>
</Badge>
);
}
return (
<Badge
color={badgeColor}
variant={badgeVariant}
badgeContent={innerBadgeContent}
componentsProps={{
badge: {
className: blink ? 'animate-pulse' : '',
},
}}
{...props}
>
{children}
</Badge>
);
}

View File

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

View File

@@ -0,0 +1,130 @@
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { Tooltip, tooltipClasses } from '@/components/ui/v2/Tooltip';
import { ProjectHealthBadge } from '@/features/projects/overview/components/ProjectHealthBadge';
import { serviceStateToBadgeColor } from '@/features/projects/overview/health';
import { ServiceState } from '@/utils/__generated__/graphql';
import type { ImageProps } from 'next/image';
import Image from 'next/image';
import type { ReactElement } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ProjectHealthCardProps extends BoxProps {
/**
* Label of the card icon.
*/
alt?: string;
/**
* Tooltip of the card.
*/
tooltip?: ReactElement | null;
/**
* Icon to display on the card.
*/
icon: string | ReactElement;
/**
* Light version of the icon. This is used for the dark mode.
*/
lightIcon?: string | ReactElement;
/**
* Determines whether the icon should have a background.
* @default false
*/
disableIconBackground?: boolean;
/**
* Determines whether the icon is a react component.
* @default true
*/
iconIsComponent?: boolean;
/**
* Props to be passed to the internal components.
*/
slotProps?: {
imgIcon?: Partial<ImageProps>;
};
/**
* State of the service.
*/
state?: ServiceState;
/**
* Determines whether the version is mismatched with recommended version.
*/
isVersionMismatch?: boolean;
/**
* Determines whether the card is loading.
*/
isLoading?: boolean;
}
export default function ProjectHealthCard({
alt,
tooltip,
icon,
iconIsComponent = true,
className,
slotProps = {},
isVersionMismatch = false,
isLoading = false,
state,
...props
}: ProjectHealthCardProps) {
const badgeColor = serviceStateToBadgeColor.get(state);
const unknownState = state === undefined;
let badgeVariant: 'dot' | 'standard' = 'dot';
if (state === ServiceState.Running || unknownState) {
badgeVariant = 'standard';
}
const showCheckIcon = state === ServiceState.Running;
const shouldBlink = state === ServiceState.Updating;
return (
<Tooltip
title={tooltip}
slotProps={{
popper: {
sx: {
[`&.${tooltipClasses.popper} .${tooltipClasses.tooltip}`]: {
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.100' : 'grey.200',
minWidth: '18rem',
},
},
},
}}
>
<Box
className={twMerge(
'grid aspect-square min-w-12 max-w-14 grid-flow-row gap-0 rounded-md p-0',
className,
)}
sx={{ backgroundColor: 'grey.200' }}
{...props}
>
<div className="grid grid-flow-col items-center justify-center">
<ProjectHealthBadge
badgeColor={!isLoading ? badgeColor : undefined}
badgeVariant={badgeVariant}
showCheckIcon={showCheckIcon}
showExclamation={isVersionMismatch}
unknownState={unknownState}
blink={shouldBlink}
>
{iconIsComponent
? icon
: typeof icon === 'string' && (
<Image
src={icon}
alt={alt}
width={slotProps.imgIcon?.width}
height={slotProps.imgIcon?.height}
{...slotProps.imgIcon}
/>
)}
</ProjectHealthBadge>
</div>
</Box>
</Tooltip>
);
}

View File

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

View File

@@ -0,0 +1,57 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import {
serviceStateToThemeColor,
type baseServices,
type ServiceHealthInfo,
} from '@/features/projects/overview/health';
import { ServiceState } from '@/generated/graphql';
export interface RunStatusTooltipProps {
servicesStatusInfo?: Array<ServiceHealthInfo>;
openHealthModal?: (
defaultExpanded?: keyof typeof baseServices | 'run',
) => void;
}
export default function RunStatusTooltip({
servicesStatusInfo,
openHealthModal,
}: RunStatusTooltipProps) {
return (
<div className="flex w-full flex-col gap-3 px-2 py-3">
<ol className="m-0 flex flex-col gap-3">
{servicesStatusInfo.map((service) => (
<li
key={service.name}
className="flex flex-row items-center gap-4 text-ellipsis text-nowrap leading-5"
>
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(service.state),
}}
className={`h-3 w-3 flex-shrink-0 rounded-full ${
service.state === ServiceState.Updating ? 'animate-pulse' : ''
}`}
/>
<Text
sx={{
color: (theme) =>
theme.palette.mode === 'dark'
? 'text.primary'
: 'text.primary',
}}
className="font-semibold"
>
{service.name}
</Text>
</li>
))}
</ol>
<Button variant="outlined" onClick={() => openHealthModal('run')}>
View state
</Button>
</div>
);
}

View File

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

View File

@@ -0,0 +1,95 @@
import { CodeBlock } from '@/components/presentational/CodeBlock';
import { Accordion } from '@/components/ui/v2/Accordion';
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { Text } from '@/components/ui/v2/Text';
import { AccordionHealthBadge } from '@/features/projects/overview/components/AccordionHealthBadge';
import { ServiceState } from '@/utils/__generated__/graphql';
import Image from 'next/image';
import { type ReactElement } from 'react';
export interface ServiceAccordionProps {
serviceName: string;
serviceInfo: string;
replicaCount: number;
serviceState: ServiceState;
/**
* Icon to display on the accordion.
*/
icon?: string | ReactElement;
/**
* Label of the icon.
*/
alt?: string;
iconIsComponent?: boolean;
defaultExpanded?: boolean;
}
export default function ServiceAccordion({
serviceName,
serviceInfo,
replicaCount,
serviceState,
icon,
iconIsComponent = true,
alt,
defaultExpanded = false,
}: ServiceAccordionProps) {
const unknownState = serviceState === undefined;
const replicasLabel = replicaCount === 1 ? 'replica' : 'replicas';
const blink = serviceState === ServiceState.Updating;
return (
<Accordion.Root defaultExpanded={defaultExpanded}>
<Accordion.Summary
expandIcon={
<ChevronDownIcon
sx={{
color: 'text.primary',
}}
/>
}
aria-controls="panel1-content"
id="panel1-header"
className="px-6"
>
<div className="flex flex-row justify-between gap-2 py-2">
<div className="flex items-center gap-3">
{iconIsComponent
? icon
: typeof icon === 'string' && <Image src={icon} alt={alt} />}
<Text
sx={{ color: 'text.primary' }}
variant="h4"
className="font-semibold"
>
{serviceName}{' '}
{!unknownState && replicaCount && replicasLabel ? (
<Text
sx={{
color: 'text.secondary',
}}
component="span"
className="font-semibold"
>
({replicaCount} {replicasLabel})
</Text>
) : null}
</Text>
<AccordionHealthBadge
serviceState={serviceState}
unknownState={unknownState}
blink={blink}
/>
</div>
</div>
</Accordion.Summary>
<Accordion.Details>
<CodeBlock copyToClipboardToastTitle={`${serviceName} status`}>
{serviceInfo}
</CodeBlock>
</Accordion.Details>
</Accordion.Root>
);
}

View File

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

View File

@@ -0,0 +1,161 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import type { baseServices } from '@/features/projects/overview/health';
import { ServiceState } from '@/utils/__generated__/graphql';
import { useTheme } from '@mui/material';
interface ServiceVersionTooltipProps {
serviceName?: string;
serviceKey?: keyof typeof baseServices;
usedVersion?: string;
recommendedVersionMismatch?: boolean;
recommendedVersions?: string[];
children?: React.ReactNode;
openHealthModal?: (
defaultExpanded?: keyof typeof baseServices | 'run',
) => void;
state?: ServiceState;
}
function ServiceVersionTooltip({
serviceName,
usedVersion,
recommendedVersionMismatch,
recommendedVersions,
children,
openHealthModal,
state,
serviceKey,
}: ServiceVersionTooltipProps) {
const theme = useTheme();
let errorMessage = '';
if (state === ServiceState.UpdateError) {
errorMessage =
'degraded or failed to update, click on view state for further details';
} else if (state === ServiceState.Error || state === ServiceState.None) {
errorMessage =
'is offline due to errors, click on view state for further details';
}
return (
<div className="flex flex-col gap-3 px-2 py-3">
<div className="flex flex-row justify-between gap-6">
<Text
sx={{
color:
theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.secondary',
}}
variant="h4"
component="p"
className="text-sm+"
>
service
</Text>
<Text
sx={{
color:
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
}}
variant="h4"
component="p"
className="text-sm+ font-semibold"
>
{serviceName}
</Text>
</div>
<div className="flex flex-row justify-between gap-6">
<Text
sx={{
color:
theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.secondary',
}}
variant="h4"
component="p"
className="text-sm+"
>
version
</Text>
<Text
sx={{
color:
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
}}
variant="h4"
component="p"
className="text-sm+ font-bold"
>
{usedVersion}
</Text>
</div>
{recommendedVersionMismatch && (
<Box
sx={{
backgroundColor:
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.300',
}}
className="rounded-md p-2"
>
<Text
sx={{
color:
theme.palette.mode === 'dark' ? 'text.primary' : 'text.primary',
}}
variant="body1"
component="p"
className="text-sm+"
>
{serviceName} is not using a recommended version. Recommended
version(s):
</Text>
<ul className="list-disc text-sm+">
{recommendedVersions.map((version) => (
<li className="ml-6 list-item" key={version}>
<Text
sx={{
color:
theme.palette.mode === 'dark'
? 'text.primary'
: 'text.primary',
}}
variant="body1"
component="p"
>
{version}
</Text>
</li>
))}
</ul>
</Box>
)}
{errorMessage ? (
<Box
sx={{
backgroundColor:
theme.palette.mode === 'dark' ? 'error.dark' : 'error.main',
}}
className="rounded-md p-2"
>
<Text
variant="body1"
component="p"
className="text-sm+ font-semibold text-white"
>
{serviceName} {errorMessage}
</Text>
</Box>
) : null}
<Button variant="outlined" onClick={() => openHealthModal(serviceKey)}>
View state
</Button>
{children}
</div>
);
}
export default ServiceVersionTooltip;

View File

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

View File

@@ -0,0 +1,80 @@
import {
ServiceState,
type GetProjectServicesHealthQuery,
} from '@/utils/__generated__/graphql';
export type ServiceHealthInfo =
GetProjectServicesHealthQuery['getProjectStatus']['services'][number];
export const baseServices = {
'hasura-auth': {
displayName: 'Auth',
softwareVersionsName: 'Auth',
},
hasura: {
displayName: 'Hasura',
softwareVersionsName: 'Hasura',
},
postgres: {
displayName: 'Postgres',
softwareVersionsName: 'PostgreSQL',
},
'hasura-storage': {
displayName: 'Storage',
softwareVersionsName: 'Storage',
},
ai: {
displayName: 'Graphite',
softwareVersionsName: 'Graphite',
},
} as const;
export const serviceStateToThemeColor = new Map<ServiceState, string>([
[ServiceState.Running, 'success.dark'],
[ServiceState.Error, 'error.main'],
[ServiceState.UpdateError, 'error.main'],
[ServiceState.Updating, 'warning.dark'],
[ServiceState.None, 'error.main'],
[undefined, 'grey.500'],
]);
export const serviceStateToBadgeColor = new Map<
ServiceState,
'success' | 'error' | 'warning' | 'secondary'
>([
[ServiceState.Running, 'success'],
[ServiceState.Error, 'error'],
[ServiceState.UpdateError, 'error'],
[ServiceState.Updating, 'warning'],
[ServiceState.None, 'error'],
[undefined, 'secondary'], // secondary is used for unknown states
]);
/**
* Returns the highest importance state from a list of service states
* Example: [Running, Running, Error] => Error
*/
export const findHighestImportanceState = (
servicesStates: ServiceState[],
): ServiceState => {
const serviceStateToImportance = new Map([
[ServiceState.Running, 0],
[ServiceState.Updating, 1],
[ServiceState.UpdateError, 2],
[ServiceState.Error, 3],
[ServiceState.None, 4],
]);
if (servicesStates.length === 0) {
return ServiceState.None;
}
return servicesStates.reduce((acc, state) => {
if (
serviceStateToImportance.get(state) > serviceStateToImportance.get(acc)
) {
return state;
}
return acc;
}, ServiceState.Running);
};

View File

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

View File

@@ -190,7 +190,7 @@ export default function ServiceForm({
image:
values.image.length > 0
? values.image
: `registry.${currentProject.region.awsName}.${currentProject.region.domain}/${newServiceID}`,
: `registry.${currentProject.region.name}.${currentProject.region.domain}/${newServiceID}`,
},
},
},
@@ -361,7 +361,7 @@ export default function ServiceForm({
{isPlatform && serviceID && serviceImage && (
<InfoCard
title="Private registry"
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
value={`registry.${currentProject.region.name}.${currentProject.region.domain}/${serviceID}`}
/>
)}

View File

@@ -43,7 +43,7 @@ export default function PortsFormSection() {
const getPortURL = (_port: string | number, subdomain: string) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
};
return (
@@ -107,7 +107,7 @@ export default function PortsFormSection() {
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
className: 'z-[10000] w-[270px]',
},
}}
>

View File

@@ -35,7 +35,7 @@ export default function ServiceDetailsDialog({
const getPortURL = (_port: string | number) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
};
return (
@@ -44,7 +44,7 @@ export default function ServiceDetailsDialog({
<Text color="secondary">Private registry</Text>
<InfoCard
title=""
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
value={`registry.${currentProject.region.name}.${currentProject.region.domain}/${serviceID}`}
/>
</div>

View File

@@ -0,0 +1,19 @@
query getConfiguredVersions($appId: uuid!) {
config(appID: $appId, resolve: true) {
auth {
version
}
postgres {
version
}
hasura {
version
}
ai {
version
}
storage {
version
}
}
}

View File

@@ -0,0 +1,20 @@
query getProjectServicesHealth($appId: String!) {
getProjectStatus(appID: $appId) {
services {
name
state
replicas {
ready
date
errors {
name
lastError {
reason
exitCode
message
}
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
query getConfigRawJSON ($appID: uuid!) {
configRawJSON(appID: $appID, resolve: false)
}

View File

@@ -0,0 +1,3 @@
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
}

View File

@@ -39,7 +39,7 @@ fragment Project on apps {
region {
id
countryCode
awsName
name
domain
city
}

View File

@@ -0,0 +1,6 @@
query getRecommendedSoftwareVersions {
softwareVersions {
software
version
}
}

View File

@@ -0,0 +1,11 @@
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { TOMLEditor } from '@/features/projects/common/components/settings/TOMLEditor';
import type { ReactElement } from 'react';
export default function TOMLEditorPage() {
return <TOMLEditor />;
}
TOMLEditorPage.getLayout = function getLayout(page: ReactElement) {
return <SettingsLayout>{page}</SettingsLayout>;
};

View File

@@ -7,41 +7,55 @@ import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { WorkspaceAndProjectList } from '@/features/projects/common/components/WorkspaceAndProjectList';
import { WorkspaceSidebar } from '@/features/projects/common/components/WorkspaceSidebar';
import { useGetAllWorkspacesAndProjectsQuery } from '@/utils/__generated__/graphql';
import {
useGetAllWorkspacesAndProjectsQuery,
type GetAllWorkspacesAndProjectsQuery,
} from '@/utils/__generated__/graphql';
import { NetworkStatus } from '@apollo/client';
import { darken } from '@mui/system';
import { useUserData } from '@nhost/nextjs';
import NavLink from 'next/link';
import type { ReactElement } from 'react';
import { useEffect } from 'react';
import { useState, type ReactElement } from 'react';
export default function IndexPage() {
const user = useUserData();
const { data, loading, startPolling, stopPolling } =
const [, setPollInterval] = useState(1_000);
// keep polling for workspaces until there is a workspace available.
// We do this because when a user signs up a workspace is created automatically
// and the serverless function can take some time to complete.
const { data, startPolling, stopPolling, networkStatus } =
useGetAllWorkspacesAndProjectsQuery({
skip: !user,
notifyOnNetworkStatusChange: true,
onError: () => {
// When there's an error (graphql, network error) apply an exponential backoff strategy
setPollInterval((prevInterval) => {
const newInterval = Math.min(60_000, prevInterval * 2);
startPolling(newInterval);
return newInterval;
});
},
onCompleted: (queryData: GetAllWorkspacesAndProjectsQuery) => {
if (!queryData?.workspaces.length) {
setPollInterval(1000);
startPolling(1000);
} else {
setPollInterval(0);
stopPolling();
}
},
});
// keep showing loading indicator while polling
const loading = networkStatus === NetworkStatus.loading;
const numberOfProjects = data?.workspaces.reduce(
(projectCount, currentWorkspace) =>
projectCount + currentWorkspace.projects.length,
0,
);
// keep polling for workspaces until there is a workspace available.
// We do this because when a user signs up a workspace is created automatically
// and the serverless function can take some time to complete.
useEffect(() => {
startPolling(1000);
}, [startPolling]);
useEffect(() => {
if (!data?.workspaces.length) {
return;
}
stopPolling();
}, [data?.workspaces, stopPolling]);
if ((!data && loading) || !user) {
return <LoadingScreen />;
}

View File

@@ -301,7 +301,7 @@ export function NewProjectPageContent({
const regionInList = regions.find(({ id }) => id === value);
setSelectedRegion({
id: regionInList.id,
name: regionInList.country.name,
name: regionInList.city,
disabled: false,
code: regionInList.country.code,
});

View File

@@ -0,0 +1,128 @@
import { Logo } from '@/components/presentational/Logo';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { CommunityIcon } from '@/components/ui/v2/icons/CommunityIcon';
import { FileTextIcon } from '@/components/ui/v2/icons/FileTextIcon';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
function SupportPage() {
return (
<Box className="h-screen pb-4 overflow-auto">
<Box className="flex justify-start w-full px-4 py-3 border-b-1">
<Logo className="cursor-pointer" />
</Box>
<div className="flex flex-col items-center justify-center">
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex flex-col items-center justify-center w-full h-64 gap-10 px-4 mb-10 border-b-1"
>
<div>
<Text variant="h4">Nhost Support</Text>
<Text variant="h2">How can we help?</Text>
</div>
<Button
onClick={() => window.open('https://docs.nhost.io')}
className="h-10 w-full xs+:w-98"
startIcon={<FileTextIcon className="self-center w-4 h-4" />}
>
Read our docs
</Button>
</Box>
<Box className="flex flex-row items-center justify-center w-full gap-10">
<div className="flex w-[900px] flex-col gap-10 p-4">
<div className="flex flex-col w-full gap-10 md:flex-row">
<Box
className="flex flex-col w-full h-full gap-12 px-4 py-3 rounded-lg shadow-sm place-content-between"
sx={{ backgroundColor: 'grey.200' }}
>
<div className="flex flex-col gap-4">
<GitHubIcon className="w-8 h-8" />
<div className="grid grid-flow-row gap-1">
<Text variant="h3" className="!font-bold">
Issues & feature requests
</Text>
<Text className="!font-medium" color="secondary">
Found a bug? We&apos;d love to hear about it in our GitHub
issues.
</Text>
</div>
</div>
<Link
variant="body2"
underline="hover"
href="https://github.com/nhost/nhost/issues/new/choose"
target="_blank"
rel="dofollow"
className="grid items-center justify-start grid-flow-col gap-1 font-medium"
>
Open new Issue / Feature request
<ArrowRightIcon className="w-4 h-4" />
</Link>
</Box>
<Box
className="flex flex-col w-full h-full gap-12 px-4 py-3 rounded-lg shadow-sm place-content-between"
sx={{ backgroundColor: 'grey.200' }}
>
<div className="flex flex-col gap-4">
<CommunityIcon className="w-8 h-8" />
<div className="grid grid-flow-row gap-1">
<Text variant="h3" className="!font-bold">
Ask the Community
</Text>
<Text className="!font-medium" color="secondary">
Join our Discord server to browse for help and best
practices.
</Text>
</div>
</div>
<Link
variant="body2"
underline="hover"
href="https://discord.com/invite/9V7Qb2U"
target="_blank"
rel="dofollow"
className="grid items-center justify-start grid-flow-col gap-1 font-medium"
>
Join our Discord
<ArrowRightIcon className="w-4 h-4" />
</Link>
</Box>
</div>
<Box className="flex h-full w-full flex-col place-content-between gap-4 rounded-lg border p-4 shadow-sm xs+:flex-row">
<div className="flex flex-1">
<Text variant="h3" className="w-full">
Can&apos;t find what you&apos;re looking for?
</Text>
</div>
<div className="flex flex-col flex-1 gap-4">
<Text variant="h4">Our Support Team is ready to help.</Text>
<Text>
Response time for support tickets will vary depending on plan
type and severity of the issue.
</Text>
<Link
variant="body2"
underline="hover"
href="/support/ticket"
target="_blank"
rel="dofollow"
className="grid items-center justify-start grid-flow-col gap-1 font-medium"
>
Create ticket
<ArrowRightIcon className="w-4 h-4" />
</Link>
</div>
</Box>
</div>
</Box>
</div>
</Box>
);
}
export default SupportPage;

View File

@@ -0,0 +1,378 @@
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Form } from '@/components/form/Form';
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { EnvelopeIcon } from '@/components/ui/v2/icons/EnvelopeIcon';
import { Input, inputClasses } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
useGetAllWorkspacesAndProjectsQuery,
type GetAllWorkspacesAndProjectsQuery,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { useUserData } from '@nhost/nextjs';
import { type ReactElement } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
type Workspace = Omit<
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
'__typename'
>;
const validationSchema = Yup.object({
workspace: Yup.string().label('Project').required(),
project: Yup.string().label('Project').required(),
services: Yup.array()
.of(Yup.object({ label: Yup.string(), value: Yup.string() }))
.label('Services')
.required(),
priority: Yup.string().label('Priority').required(),
subject: Yup.string().label('Subject').required(),
description: Yup.string().label('Description').required(),
ccs: Yup.string().label('CCs').optional(),
});
export type CreateTicketFormValues = Yup.InferType<typeof validationSchema>;
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
function TicketPage() {
const form = useForm<CreateTicketFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
workspace: '',
project: '',
services: [],
priority: '',
subject: '',
description: '',
ccs: '',
},
resolver: yupResolver(validationSchema),
});
const {
register,
watch,
formState: { errors, isSubmitting },
} = form;
const selectedWorkspace = watch('workspace');
const user = useUserData();
const { data } = useGetAllWorkspacesAndProjectsQuery({
skip: !user,
});
const workspaces: Workspace[] = data?.workspaces || [];
const handleSubmit = async (formValues: CreateTicketFormValues) => {
const { project, services, priority, subject, description, ccs } =
formValues;
const auth = btoa(
`${process.env.NEXT_PUBLIC_ZENDESK_USER_EMAIL}/token:${process.env.NEXT_PUBLIC_ZENDESK_API_KEY}`,
);
const emails = ccs
.replace(/ /g, '')
.split(',')
.map((email) => ({ user_email: email }));
await execPromiseWithErrorToast(
async () => {
await fetch(
`${process.env.NEXT_PUBLIC_ZENDESK_URL}/api/v2/requests.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${auth}`,
},
body: JSON.stringify({
request: {
subject,
comment: {
body: description,
},
priority,
requester: {
name: user?.displayName,
email: user?.email,
},
email_ccs: emails,
custom_fields: [
// these custom field IDs come from zendesk
{
id: 19502784542098,
value: project,
},
{
id: 19922709880978,
value: services.map((service) =>
service.value.toLowerCase(),
),
},
],
},
}),
},
);
form.reset();
},
{
loadingMessage: 'Creating Ticket...',
successMessage: 'Ticket created successfully',
errorMessage: 'Failed to create ticket',
},
);
};
return (
<Box
className="flex flex-col items-center justify-center py-10"
sx={{ backgroundColor: 'background.default' }}
>
<div className="flex w-full max-w-3xl flex-col">
<div className="mb-4 flex flex-col items-center">
<Text variant="h4" className="font-bold">
Nhost Support
</Text>
<Text variant="h4">How can we help you?</Text>
</div>
<Box className="w-full rounded-md border p-10">
<Box className="grid grid-flow-row gap-4">
<Box className="flex flex-col gap-4">
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4"
>
<Text className="font-bold">Which project is affected ?</Text>
<ControlledSelect
id="workspace"
name="workspace"
label="Workspace"
placeholder="Workspace"
slotProps={{
root: { className: 'grid grid-flow-col gap-1' },
}}
error={!!errors.workspace}
helperText={errors.workspace?.message}
renderValue={(option) => (
<span className="inline-grid grid-flow-col items-center gap-2">
{option?.label}
</span>
)}
>
{workspaces.map((workspace) => (
<Option
key={workspace.name}
value={workspace.id}
label={workspace.name}
>
{workspace.name}
</Option>
))}
</ControlledSelect>
<ControlledSelect
id="project"
name="project"
label="Project"
placeholder="Project"
slotProps={{
root: { className: 'grid grid-flow-col gap-1 mb-4' },
}}
error={!!errors.project}
helperText={errors.project?.message}
renderValue={(option) => (
<span className="inline-grid grid-flow-col items-center gap-2">
{option?.label}
</span>
)}
>
{(
workspaces.find((w) => w.id === selectedWorkspace)
?.projects || []
).map((proj) => (
<Option
key={proj.subdomain}
value={proj.subdomain}
label={proj.name}
>
<div className="flex flex-col">{proj.name}</div>
</Option>
))}
</ControlledSelect>
<Divider />
<Text className="mt-4 font-bold">Impact</Text>
<ControlledAutocomplete
id="services"
name="services"
label="services"
fullWidth
multiple
aria-label="Enabled APIs"
options={[
'Dashboard',
'Database',
'Authentication',
'Storage',
'Hasura/APIs',
'Functions',
'Run',
'Graphite',
'Other',
].map((s) => ({ label: s, value: s }))}
error={!!errors?.services?.message}
helperText={errors?.services?.message}
/>
<ControlledSelect
id="priority"
name="priority"
label="Priority"
placeholder="Priority"
slotProps={{
root: { className: 'grid grid-flow-col gap-1 mb-4' },
}}
renderValue={(option) => (
<span className="inline-grid grid-flow-col items-center gap-2">
{option?.label}
</span>
)}
>
{[
{
title: 'Low',
description: 'General guidance',
},
{
title: 'Normal',
description: 'Non-production system impaired',
},
{
title: 'High',
description: 'Production System impaired',
},
{
title: 'Urgent',
description: 'Production system offline',
},
].map((priority) => (
<Option
key={priority.title}
label={priority.title}
value={priority.title.toLowerCase()}
>
<div className="flex flex-col">
<span>{priority.title}</span>
<span className="font-mono text-xs opacity-50">
{priority.description}
</span>
</div>
</Option>
))}
</ControlledSelect>
<Divider />
<Text className="mt-4 font-bold">Issue</Text>
<StyledInput
{...register('subject')}
id="subject"
label="Subject"
placeholder="Summary of the problem you are experiencing"
fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }}
error={!!errors.subject}
helperText={errors.subject?.message}
/>
<StyledInput
{...register('description')}
id="description"
label="Description"
placeholder="Describe the issue you are experiencing in detail, along with any relevant information. Please be as detailed as possible."
fullWidth
multiline
inputProps={{
className: 'resize-y min-h-[120px]',
}}
error={!!errors.description}
helperText={errors.description?.message}
/>
<Divider />
<Text className="mt-4 font-bold">Notifications</Text>
<StyledInput
{...register('ccs')}
id="ccs"
label="CCs"
placeholder="Comma separated list of emails you want to share this ticket with."
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!errors.ccs}
helperText={errors.ccs?.message}
/>
<Box className="ml-auto flex w-80 flex-col gap-4">
<Text color="secondary" className="text-right text-sm">
We will contact you at <strong>{user?.email}</strong>
</Text>
<Button
variant="outlined"
className=" hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
size="large"
type="submit"
startIcon={<EnvelopeIcon />}
disabled={isSubmitting}
loading={isSubmitting}
>
Create Support Ticket
</Button>
</Box>
</Form>
</FormProvider>
</Box>
</Box>
</Box>
</div>
</Box>
);
}
TicketPage.getLayout = function getLayout(page: ReactElement) {
return (
<AuthenticatedLayout
title="Help & Support | Nhost"
contentContainerProps={{
className: 'flex w-full flex-col h-screen overflow-auto',
}}
>
{page}
</AuthenticatedLayout>
);
};
export default TicketPage;

View File

@@ -50,7 +50,7 @@ export const mockApplication: Project = {
appStates: [],
subdomain: '',
region: {
awsName: 'us-east-1',
name: 'us-east-1',
city: 'New York',
countryCode: 'US',
id: '1',

View File

@@ -1037,6 +1037,26 @@ export type ConfigAuthsessionaccessTokenCustomClaimsUpdateInput = {
value?: InputMaybe<Scalars['String']>;
};
export type ConfigAutoscaler = {
__typename?: 'ConfigAutoscaler';
maxReplicas: Scalars['ConfigUint8'];
};
export type ConfigAutoscalerComparisonExp = {
_and?: InputMaybe<Array<ConfigAutoscalerComparisonExp>>;
_not?: InputMaybe<ConfigAutoscalerComparisonExp>;
_or?: InputMaybe<Array<ConfigAutoscalerComparisonExp>>;
maxReplicas?: InputMaybe<ConfigUint8ComparisonExp>;
};
export type ConfigAutoscalerInsertInput = {
maxReplicas: Scalars['ConfigUint8'];
};
export type ConfigAutoscalerUpdateInput = {
maxReplicas?: InputMaybe<Scalars['ConfigUint8']>;
};
export type ConfigBooleanComparisonExp = {
_eq?: InputMaybe<Scalars['Boolean']>;
_in?: InputMaybe<Array<Scalars['Boolean']>>;
@@ -1803,6 +1823,7 @@ export type ConfigPostgresInsertInput = {
/** Resources for the service */
export type ConfigPostgresResources = {
__typename?: 'ConfigPostgresResources';
autoscaler?: Maybe<ConfigAutoscaler>;
compute?: Maybe<ConfigResourcesCompute>;
enablePublicAccess?: Maybe<Scalars['Boolean']>;
networking?: Maybe<ConfigNetworking>;
@@ -1815,6 +1836,7 @@ export type ConfigPostgresResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigPostgresResourcesComparisonExp>>;
_not?: InputMaybe<ConfigPostgresResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigPostgresResourcesComparisonExp>>;
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
compute?: InputMaybe<ConfigResourcesComputeComparisonExp>;
enablePublicAccess?: InputMaybe<ConfigBooleanComparisonExp>;
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
@@ -1823,6 +1845,7 @@ export type ConfigPostgresResourcesComparisonExp = {
};
export type ConfigPostgresResourcesInsertInput = {
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
compute?: InputMaybe<ConfigResourcesComputeInsertInput>;
enablePublicAccess?: InputMaybe<Scalars['Boolean']>;
networking?: InputMaybe<ConfigNetworkingInsertInput>;
@@ -1831,6 +1854,7 @@ export type ConfigPostgresResourcesInsertInput = {
};
export type ConfigPostgresResourcesUpdateInput = {
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
compute?: InputMaybe<ConfigResourcesComputeUpdateInput>;
enablePublicAccess?: InputMaybe<Scalars['Boolean']>;
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
@@ -1992,6 +2016,7 @@ export type ConfigProviderUpdateInput = {
/** Resource configuration for a service */
export type ConfigResources = {
__typename?: 'ConfigResources';
autoscaler?: Maybe<ConfigAutoscaler>;
compute?: Maybe<ConfigResourcesCompute>;
networking?: Maybe<ConfigNetworking>;
/** Number of replicas for a service */
@@ -2002,6 +2027,7 @@ export type ConfigResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigResourcesComparisonExp>>;
_not?: InputMaybe<ConfigResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigResourcesComparisonExp>>;
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
compute?: InputMaybe<ConfigResourcesComputeComparisonExp>;
networking?: InputMaybe<ConfigNetworkingComparisonExp>;
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
@@ -2034,12 +2060,14 @@ export type ConfigResourcesComputeUpdateInput = {
};
export type ConfigResourcesInsertInput = {
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
compute?: InputMaybe<ConfigResourcesComputeInsertInput>;
networking?: InputMaybe<ConfigNetworkingInsertInput>;
replicas?: InputMaybe<Scalars['ConfigUint8']>;
};
export type ConfigResourcesUpdateInput = {
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
compute?: InputMaybe<ConfigResourcesComputeUpdateInput>;
networking?: InputMaybe<ConfigNetworkingUpdateInput>;
replicas?: InputMaybe<Scalars['ConfigUint8']>;
@@ -2157,6 +2185,7 @@ export type ConfigRunServicePortUpdateInput = {
/** Resource configuration for a service */
export type ConfigRunServiceResources = {
__typename?: 'ConfigRunServiceResources';
autoscaler?: Maybe<ConfigAutoscaler>;
compute: ConfigComputeResources;
/** Number of replicas for a service */
replicas: Scalars['ConfigUint8'];
@@ -2167,12 +2196,14 @@ export type ConfigRunServiceResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
_not?: InputMaybe<ConfigRunServiceResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
autoscaler?: InputMaybe<ConfigAutoscalerComparisonExp>;
compute?: InputMaybe<ConfigComputeResourcesComparisonExp>;
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
storage?: InputMaybe<ConfigRunServiceResourcesStorageComparisonExp>;
};
export type ConfigRunServiceResourcesInsertInput = {
autoscaler?: InputMaybe<ConfigAutoscalerInsertInput>;
compute: ConfigComputeResourcesInsertInput;
replicas: Scalars['ConfigUint8'];
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageInsertInput>>;
@@ -2209,6 +2240,7 @@ export type ConfigRunServiceResourcesStorageUpdateInput = {
};
export type ConfigRunServiceResourcesUpdateInput = {
autoscaler?: InputMaybe<ConfigAutoscalerUpdateInput>;
compute?: InputMaybe<ConfigComputeResourcesUpdateInput>;
replicas?: InputMaybe<Scalars['ConfigUint8']>;
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageUpdateInput>>;
@@ -2657,6 +2689,12 @@ export type ConfigUserRoleComparisonExp = {
_nin?: InputMaybe<Array<Scalars['ConfigUserRole']>>;
};
export type ContainerError = {
__typename?: 'ContainerError';
lastError: LastError;
name: Scalars['String'];
};
/** Boolean expression to compare columns of type "Int". All fields are combined with logical 'AND'. */
export type Int_Comparison_Exp = {
_eq?: InputMaybe<Scalars['Int']>;
@@ -2683,6 +2721,13 @@ export type InvoiceSummary = {
items: Array<InvoiceItem>;
};
export type LastError = {
__typename?: 'LastError';
exitCode: Scalars['Int'];
message: Scalars['String'];
reason: Scalars['String'];
};
export type Log = {
__typename?: 'Log';
log: Scalars['String'];
@@ -2695,6 +2740,33 @@ export type Metrics = {
value: Scalars['float64'];
};
export type ProjectStatusResponse = {
__typename?: 'ProjectStatusResponse';
services: Array<ServiceStatus>;
};
export type ReplicaStatus = {
__typename?: 'ReplicaStatus';
date: Scalars['Timestamp'];
errors: Array<ContainerError>;
ready: Scalars['Boolean'];
};
export enum ServiceState {
Error = 'Error',
None = 'None',
Running = 'Running',
UpdateError = 'UpdateError',
Updating = 'Updating'
}
export type ServiceStatus = {
__typename?: 'ServiceStatus';
name: Scalars['String'];
replicas: Array<ReplicaStatus>;
state: ServiceState;
};
export type StatsLiveApps = {
__typename?: 'StatsLiveApps';
appID: Array<Scalars['uuid']>;
@@ -12172,6 +12244,7 @@ export type Mutation_Root = {
pauseAppsExceedUsage: Array<Scalars['String']>;
pauseInactiveApps: Array<Scalars['String']>;
replaceConfig: ConfigConfig;
replaceConfigRawJSON: Scalars['String'];
replaceRunServiceConfig: ConfigRunServiceConfig;
resetPostgresPassword: Scalars['Boolean'];
restoreApplicationDatabase: Scalars['Boolean'];
@@ -13663,6 +13736,13 @@ export type Mutation_RootReplaceConfigArgs = {
};
/** mutation root */
export type Mutation_RootReplaceConfigRawJsonArgs = {
appID: Scalars['uuid'];
rawJSON: Scalars['String'];
};
/** mutation root */
export type Mutation_RootReplaceRunServiceConfigArgs = {
appID: Scalars['uuid'];
@@ -15169,6 +15249,7 @@ export type Plans = {
deprecated: Scalars['Boolean'];
featureAdvancedGraphql: Scalars['Boolean'];
featureBackupEnabled: Scalars['Boolean'];
featureBackupRetentionDays: Scalars['Int'];
featureCustomDomainsEnabled: Scalars['Boolean'];
featureCustomEmailTemplatesEnabled: Scalars['Boolean'];
featureCustomResources: Scalars['Boolean'];
@@ -15251,6 +15332,7 @@ export type Plans_Aggregate_FieldsCountArgs = {
/** aggregate avg on columns */
export type Plans_Avg_Fields = {
__typename?: 'plans_avg_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15272,6 +15354,7 @@ export type Plans_Bool_Exp = {
deprecated?: InputMaybe<Boolean_Comparison_Exp>;
featureAdvancedGraphql?: InputMaybe<Boolean_Comparison_Exp>;
featureBackupEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureBackupRetentionDays?: InputMaybe<Int_Comparison_Exp>;
featureCustomDomainsEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureCustomResources?: InputMaybe<Boolean_Comparison_Exp>;
@@ -15306,6 +15389,7 @@ export enum Plans_Constraint {
/** input type for incrementing numeric columns in table "plans" */
export type Plans_Inc_Input = {
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: InputMaybe<Scalars['Int']>;
featureMaxDbSize?: InputMaybe<Scalars['Int']>;
@@ -15323,6 +15407,7 @@ export type Plans_Insert_Input = {
deprecated?: InputMaybe<Scalars['Boolean']>;
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
@@ -15356,6 +15441,7 @@ export type Plans_Insert_Input = {
export type Plans_Max_Fields = {
__typename?: 'plans_max_fields';
createdAt?: Maybe<Scalars['timestamptz']>;
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
featureMaxDbSize?: Maybe<Scalars['Int']>;
@@ -15380,6 +15466,7 @@ export type Plans_Max_Fields = {
export type Plans_Min_Fields = {
__typename?: 'plans_min_fields';
createdAt?: Maybe<Scalars['timestamptz']>;
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
featureMaxDbSize?: Maybe<Scalars['Int']>;
@@ -15430,6 +15517,7 @@ export type Plans_Order_By = {
deprecated?: InputMaybe<Order_By>;
featureAdvancedGraphql?: InputMaybe<Order_By>;
featureBackupEnabled?: InputMaybe<Order_By>;
featureBackupRetentionDays?: InputMaybe<Order_By>;
featureCustomDomainsEnabled?: InputMaybe<Order_By>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Order_By>;
featureCustomResources?: InputMaybe<Order_By>;
@@ -15472,6 +15560,8 @@ export enum Plans_Select_Column {
/** column name */
FeatureBackupEnabled = 'featureBackupEnabled',
/** column name */
FeatureBackupRetentionDays = 'featureBackupRetentionDays',
/** column name */
FeatureCustomDomainsEnabled = 'featureCustomDomainsEnabled',
/** column name */
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
@@ -15527,6 +15617,7 @@ export type Plans_Set_Input = {
deprecated?: InputMaybe<Scalars['Boolean']>;
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
@@ -15559,6 +15650,7 @@ export type Plans_Set_Input = {
/** aggregate stddev on columns */
export type Plans_Stddev_Fields = {
__typename?: 'plans_stddev_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15572,6 +15664,7 @@ export type Plans_Stddev_Fields = {
/** aggregate stddev_pop on columns */
export type Plans_Stddev_Pop_Fields = {
__typename?: 'plans_stddev_pop_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15585,6 +15678,7 @@ export type Plans_Stddev_Pop_Fields = {
/** aggregate stddev_samp on columns */
export type Plans_Stddev_Samp_Fields = {
__typename?: 'plans_stddev_samp_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15609,6 +15703,7 @@ export type Plans_Stream_Cursor_Value_Input = {
deprecated?: InputMaybe<Scalars['Boolean']>;
featureAdvancedGraphql?: InputMaybe<Scalars['Boolean']>;
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureBackupRetentionDays?: InputMaybe<Scalars['Int']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
@@ -15641,6 +15736,7 @@ export type Plans_Stream_Cursor_Value_Input = {
/** aggregate sum on columns */
export type Plans_Sum_Fields = {
__typename?: 'plans_sum_fields';
featureBackupRetentionDays?: Maybe<Scalars['Int']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Int']>;
featureMaxDbSize?: Maybe<Scalars['Int']>;
@@ -15662,6 +15758,8 @@ export enum Plans_Update_Column {
/** column name */
FeatureBackupEnabled = 'featureBackupEnabled',
/** column name */
FeatureBackupRetentionDays = 'featureBackupRetentionDays',
/** column name */
FeatureCustomDomainsEnabled = 'featureCustomDomainsEnabled',
/** column name */
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
@@ -15723,6 +15821,7 @@ export type Plans_Updates = {
/** aggregate var_pop on columns */
export type Plans_Var_Pop_Fields = {
__typename?: 'plans_var_pop_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15736,6 +15835,7 @@ export type Plans_Var_Pop_Fields = {
/** aggregate var_samp on columns */
export type Plans_Var_Samp_Fields = {
__typename?: 'plans_var_samp_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15749,6 +15849,7 @@ export type Plans_Var_Samp_Fields = {
/** aggregate variance on columns */
export type Plans_Variance_Fields = {
__typename?: 'plans_variance_fields';
featureBackupRetentionDays?: Maybe<Scalars['Float']>;
/** Function execution timeout in seconds */
featureFunctionExecutionTimeout?: Maybe<Scalars['Float']>;
featureMaxDbSize?: Maybe<Scalars['Float']>;
@@ -15948,6 +16049,7 @@ export type Query_Root = {
getLogsVolume: Metrics;
getPostgresVolumeCapacity: Metrics;
getPostgresVolumeUsage: Metrics;
getProjectStatus: ProjectStatusResponse;
/**
* Returns list of label values for a given label within a range of time.
*
@@ -16801,6 +16903,11 @@ export type Query_RootGetPostgresVolumeUsageArgs = {
};
export type Query_RootGetProjectStatusArgs = {
appID: Scalars['String'];
};
export type Query_RootGetServiceLabelValuesArgs = {
appID: Scalars['String'];
};
@@ -17427,6 +17534,7 @@ export type Regions = {
domain: Scalars['String'];
id: Scalars['uuid'];
isGdprCompliant: Scalars['Boolean'];
name: Scalars['String'];
/** An object relationship */
region_type: Region_Type;
/** An array relationship */
@@ -17804,6 +17912,7 @@ export type Regions_Bool_Exp = {
domain?: InputMaybe<String_Comparison_Exp>;
id?: InputMaybe<Uuid_Comparison_Exp>;
isGdprCompliant?: InputMaybe<Boolean_Comparison_Exp>;
name?: InputMaybe<String_Comparison_Exp>;
region_type?: InputMaybe<Region_Type_Bool_Exp>;
regions_allowed_workspaces?: InputMaybe<Regions_Allowed_Workspace_Bool_Exp>;
regions_allowed_workspaces_aggregate?: InputMaybe<Regions_Allowed_Workspace_Aggregate_Bool_Exp>;
@@ -17831,6 +17940,7 @@ export type Regions_Insert_Input = {
domain?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['uuid']>;
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
region_type?: InputMaybe<Region_Type_Obj_Rel_Insert_Input>;
regions_allowed_workspaces?: InputMaybe<Regions_Allowed_Workspace_Arr_Rel_Insert_Input>;
type?: InputMaybe<Region_Type_Enum>;
@@ -17847,6 +17957,7 @@ export type Regions_Max_Fields = {
description?: Maybe<Scalars['String']>;
domain?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['uuid']>;
name?: Maybe<Scalars['String']>;
updatedAt?: Maybe<Scalars['timestamptz']>;
};
@@ -17859,6 +17970,7 @@ export type Regions_Max_Order_By = {
description?: InputMaybe<Order_By>;
domain?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>;
name?: InputMaybe<Order_By>;
updatedAt?: InputMaybe<Order_By>;
};
@@ -17872,6 +17984,7 @@ export type Regions_Min_Fields = {
description?: Maybe<Scalars['String']>;
domain?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['uuid']>;
name?: Maybe<Scalars['String']>;
updatedAt?: Maybe<Scalars['timestamptz']>;
};
@@ -17884,6 +17997,7 @@ export type Regions_Min_Order_By = {
description?: InputMaybe<Order_By>;
domain?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>;
name?: InputMaybe<Order_By>;
updatedAt?: InputMaybe<Order_By>;
};
@@ -17924,6 +18038,7 @@ export type Regions_Order_By = {
domain?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>;
isGdprCompliant?: InputMaybe<Order_By>;
name?: InputMaybe<Order_By>;
region_type?: InputMaybe<Region_Type_Order_By>;
regions_allowed_workspaces_aggregate?: InputMaybe<Regions_Allowed_Workspace_Aggregate_Order_By>;
type?: InputMaybe<Order_By>;
@@ -17956,6 +18071,8 @@ export enum Regions_Select_Column {
/** column name */
IsGdprCompliant = 'isGdprCompliant',
/** column name */
Name = 'name',
/** column name */
Type = 'type',
/** column name */
UpdatedAt = 'updatedAt'
@@ -17988,6 +18105,7 @@ export type Regions_Set_Input = {
domain?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['uuid']>;
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
type?: InputMaybe<Region_Type_Enum>;
updatedAt?: InputMaybe<Scalars['timestamptz']>;
};
@@ -18011,6 +18129,7 @@ export type Regions_Stream_Cursor_Value_Input = {
domain?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['uuid']>;
isGdprCompliant?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
type?: InputMaybe<Region_Type_Enum>;
updatedAt?: InputMaybe<Scalars['timestamptz']>;
};
@@ -18036,6 +18155,8 @@ export enum Regions_Update_Column {
/** column name */
IsGdprCompliant = 'isGdprCompliant',
/** column name */
Name = 'name',
/** column name */
Type = 'type',
/** column name */
UpdatedAt = 'updatedAt'
@@ -22765,7 +22886,7 @@ export type DeleteApplicationMutation = { __typename?: 'mutation_root', deleteAp
export type GetAllWorkspacesAndProjectsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetAppPlanAndGlobalPlansAppFragment = { __typename?: 'apps', id: any, subdomain: string, workspace: { __typename?: 'workspaces', id: any, paymentMethods: Array<{ __typename?: 'paymentMethods', id: any }> }, plan: { __typename?: 'plans', id: any, name: string } };
@@ -22794,6 +22915,13 @@ export type GetApplicationStateQueryVariables = Exact<{
export type GetApplicationStateQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', id: any, name: string, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }> } | null };
export type GetConfiguredVersionsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type GetConfiguredVersionsQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', auth?: { __typename?: 'ConfigAuth', version?: string | null } | null, postgres?: { __typename?: 'ConfigPostgres', version?: string | null } | null, hasura: { __typename?: 'ConfigHasura', version?: string | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null, storage?: { __typename?: 'ConfigStorage', version?: string | null } | null } | null };
export type GetProjectLocalesQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
@@ -22811,6 +22939,13 @@ export type GetProjectMetricsQueryVariables = Exact<{
export type GetProjectMetricsQuery = { __typename?: 'query_root', logsVolume: { __typename?: 'Metrics', value: any }, cpuSecondsUsage: { __typename?: 'Metrics', value: any }, functionInvocations: { __typename?: 'Metrics', value: any }, functionsDuration: { __typename?: 'Metrics', value: any }, postgresVolumeCapacity: { __typename?: 'Metrics', value: any }, postgresVolumeUsage: { __typename?: 'Metrics', value: any }, totalRequests: { __typename?: 'Metrics', value: any }, egressVolume: { __typename?: 'Metrics', value: any } };
export type GetProjectServicesHealthQueryVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetProjectServicesHealthQuery = { __typename?: 'query_root', getProjectStatus: { __typename?: 'ProjectStatusResponse', services: Array<{ __typename?: 'ServiceStatus', name: string, state: ServiceState, replicas: Array<{ __typename?: 'ReplicaStatus', ready: boolean, date: any, errors: Array<{ __typename?: 'ContainerError', name: string, lastError: { __typename?: 'LastError', reason: string, exitCode: number, message: string } }> }> }> } };
export type GetRemoteAppRolesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -22822,7 +22957,7 @@ export type GetWorkspaceAndProjectQueryVariables = Exact<{
}>;
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type InsertApplicationMutationVariables = Exact<{
app: Apps_Insert_Input;
@@ -22867,6 +23002,21 @@ export type GetEnvironmentVariablesQueryVariables = Exact<{
export type GetEnvironmentVariablesQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', global?: { __typename?: 'ConfigGlobal', environment?: Array<{ __typename?: 'ConfigGlobalEnvironmentVariable', name: string, value: string, id: string }> | null } | null, hasura: { __typename?: 'ConfigHasura', adminSecret: string, webhookSecret: string, jwtSecrets?: Array<{ __typename?: 'ConfigJWTSecret', issuer?: string | null, key?: string | null, type?: string | null, jwk_url?: any | null, header?: string | null, claims_namespace_path?: string | null, claims_namespace?: string | null, claims_format?: string | null, audience?: string | null, allowed_skew?: any | null }> | null } } | null };
export type GetConfigRawJsonQueryVariables = Exact<{
appID: Scalars['uuid'];
}>;
export type GetConfigRawJsonQuery = { __typename?: 'query_root', configRawJSON: string };
export type ReplaceConfigRawJsonMutationVariables = Exact<{
appID: Scalars['uuid'];
rawJSON: Scalars['String'];
}>;
export type ReplaceConfigRawJsonMutation = { __typename?: 'mutation_root', replaceConfigRawJSON: string };
export type PermissionVariableFragment = { __typename?: 'ConfigAuthsessionaccessTokenCustomClaims', key: string, value: string, id: string };
export type GetRolesPermissionsQueryVariables = Exact<{
@@ -23014,9 +23164,9 @@ export type GetFilesAggregateQuery = { __typename?: 'query_root', filesAggregate
export type AppStateHistoryFragment = { __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, name: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type GithubRepositoryFragment = { __typename?: 'githubRepositories', id: any, name: string, fullName: string, private: boolean, githubAppInstallation: { __typename?: 'githubAppInstallations', id: any, accountLogin?: string | null, accountType?: string | null, accountAvatarUrl?: string | null } };
@@ -23099,6 +23249,11 @@ export type GetPlansQueryVariables = Exact<{
export type GetPlansQuery = { __typename?: 'query_root', plans: Array<{ __typename?: 'plans', id: any, name: string, isFree: boolean, price: number }> };
export type GetRecommendedSoftwareVersionsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRecommendedSoftwareVersionsQuery = { __typename?: 'query_root', softwareVersions: Array<{ __typename?: 'software_versions', software: Software_Type_Enum, version: string }> };
export type GetSoftwareVersionsQueryVariables = Exact<{
software: Software_Type_Enum;
}>;
@@ -23559,7 +23714,7 @@ export const ProjectFragmentDoc = gql`
region {
id
countryCode
awsName
name
domain
city
}
@@ -24619,6 +24774,58 @@ export type GetApplicationStateQueryResult = Apollo.QueryResult<GetApplicationSt
export function refetchGetApplicationStateQuery(variables: GetApplicationStateQueryVariables) {
return { query: GetApplicationStateDocument, variables: variables }
}
export const GetConfiguredVersionsDocument = gql`
query getConfiguredVersions($appId: uuid!) {
config(appID: $appId, resolve: true) {
auth {
version
}
postgres {
version
}
hasura {
version
}
ai {
version
}
storage {
version
}
}
}
`;
/**
* __useGetConfiguredVersionsQuery__
*
* To run a query within a React component, call `useGetConfiguredVersionsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetConfiguredVersionsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetConfiguredVersionsQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetConfiguredVersionsQuery(baseOptions: Apollo.QueryHookOptions<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>(GetConfiguredVersionsDocument, options);
}
export function useGetConfiguredVersionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>(GetConfiguredVersionsDocument, options);
}
export type GetConfiguredVersionsQueryHookResult = ReturnType<typeof useGetConfiguredVersionsQuery>;
export type GetConfiguredVersionsLazyQueryHookResult = ReturnType<typeof useGetConfiguredVersionsLazyQuery>;
export type GetConfiguredVersionsQueryResult = Apollo.QueryResult<GetConfiguredVersionsQuery, GetConfiguredVersionsQueryVariables>;
export function refetchGetConfiguredVersionsQuery(variables: GetConfiguredVersionsQueryVariables) {
return { query: GetConfiguredVersionsDocument, variables: variables }
}
export const GetProjectLocalesDocument = gql`
query getProjectLocales($appId: uuid!) {
config(appID: $appId, resolve: false) {
@@ -24735,6 +24942,59 @@ export type GetProjectMetricsQueryResult = Apollo.QueryResult<GetProjectMetricsQ
export function refetchGetProjectMetricsQuery(variables: GetProjectMetricsQueryVariables) {
return { query: GetProjectMetricsDocument, variables: variables }
}
export const GetProjectServicesHealthDocument = gql`
query getProjectServicesHealth($appId: String!) {
getProjectStatus(appID: $appId) {
services {
name
state
replicas {
ready
date
errors {
name
lastError {
reason
exitCode
message
}
}
}
}
}
}
`;
/**
* __useGetProjectServicesHealthQuery__
*
* To run a query within a React component, call `useGetProjectServicesHealthQuery` and pass it any options that fit your needs.
* When your component renders, `useGetProjectServicesHealthQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetProjectServicesHealthQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetProjectServicesHealthQuery(baseOptions: Apollo.QueryHookOptions<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>(GetProjectServicesHealthDocument, options);
}
export function useGetProjectServicesHealthLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>(GetProjectServicesHealthDocument, options);
}
export type GetProjectServicesHealthQueryHookResult = ReturnType<typeof useGetProjectServicesHealthQuery>;
export type GetProjectServicesHealthLazyQueryHookResult = ReturnType<typeof useGetProjectServicesHealthLazyQuery>;
export type GetProjectServicesHealthQueryResult = Apollo.QueryResult<GetProjectServicesHealthQuery, GetProjectServicesHealthQueryVariables>;
export function refetchGetProjectServicesHealthQuery(variables: GetProjectServicesHealthQueryVariables) {
return { query: GetProjectServicesHealthDocument, variables: variables }
}
export const GetRemoteAppRolesDocument = gql`
query getRemoteAppRoles {
authRoles {
@@ -25017,6 +25277,74 @@ export type GetEnvironmentVariablesQueryResult = Apollo.QueryResult<GetEnvironme
export function refetchGetEnvironmentVariablesQuery(variables: GetEnvironmentVariablesQueryVariables) {
return { query: GetEnvironmentVariablesDocument, variables: variables }
}
export const GetConfigRawJsonDocument = gql`
query getConfigRawJSON($appID: uuid!) {
configRawJSON(appID: $appID, resolve: false)
}
`;
/**
* __useGetConfigRawJsonQuery__
*
* To run a query within a React component, call `useGetConfigRawJsonQuery` and pass it any options that fit your needs.
* When your component renders, `useGetConfigRawJsonQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetConfigRawJsonQuery({
* variables: {
* appID: // value for 'appID'
* },
* });
*/
export function useGetConfigRawJsonQuery(baseOptions: Apollo.QueryHookOptions<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>(GetConfigRawJsonDocument, options);
}
export function useGetConfigRawJsonLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>(GetConfigRawJsonDocument, options);
}
export type GetConfigRawJsonQueryHookResult = ReturnType<typeof useGetConfigRawJsonQuery>;
export type GetConfigRawJsonLazyQueryHookResult = ReturnType<typeof useGetConfigRawJsonLazyQuery>;
export type GetConfigRawJsonQueryResult = Apollo.QueryResult<GetConfigRawJsonQuery, GetConfigRawJsonQueryVariables>;
export function refetchGetConfigRawJsonQuery(variables: GetConfigRawJsonQueryVariables) {
return { query: GetConfigRawJsonDocument, variables: variables }
}
export const ReplaceConfigRawJsonDocument = gql`
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
}
`;
export type ReplaceConfigRawJsonMutationFn = Apollo.MutationFunction<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>;
/**
* __useReplaceConfigRawJsonMutation__
*
* To run a mutation, you first call `useReplaceConfigRawJsonMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useReplaceConfigRawJsonMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [replaceConfigRawJsonMutation, { data, loading, error }] = useReplaceConfigRawJsonMutation({
* variables: {
* appID: // value for 'appID'
* rawJSON: // value for 'rawJSON'
* },
* });
*/
export function useReplaceConfigRawJsonMutation(baseOptions?: Apollo.MutationHookOptions<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>(ReplaceConfigRawJsonDocument, options);
}
export type ReplaceConfigRawJsonMutationHookResult = ReturnType<typeof useReplaceConfigRawJsonMutation>;
export type ReplaceConfigRawJsonMutationResult = Apollo.MutationResult<ReplaceConfigRawJsonMutation>;
export type ReplaceConfigRawJsonMutationOptions = Apollo.BaseMutationOptions<ReplaceConfigRawJsonMutation, ReplaceConfigRawJsonMutationVariables>;
export const GetRolesPermissionsDocument = gql`
query GetRolesPermissions($appId: uuid!) {
config(appID: $appId, resolve: false) {
@@ -26316,6 +26644,44 @@ export type GetPlansQueryResult = Apollo.QueryResult<GetPlansQuery, GetPlansQuer
export function refetchGetPlansQuery(variables?: GetPlansQueryVariables) {
return { query: GetPlansDocument, variables: variables }
}
export const GetRecommendedSoftwareVersionsDocument = gql`
query getRecommendedSoftwareVersions {
softwareVersions {
software
version
}
}
`;
/**
* __useGetRecommendedSoftwareVersionsQuery__
*
* To run a query within a React component, call `useGetRecommendedSoftwareVersionsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRecommendedSoftwareVersionsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetRecommendedSoftwareVersionsQuery({
* variables: {
* },
* });
*/
export function useGetRecommendedSoftwareVersionsQuery(baseOptions?: Apollo.QueryHookOptions<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>(GetRecommendedSoftwareVersionsDocument, options);
}
export function useGetRecommendedSoftwareVersionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>(GetRecommendedSoftwareVersionsDocument, options);
}
export type GetRecommendedSoftwareVersionsQueryHookResult = ReturnType<typeof useGetRecommendedSoftwareVersionsQuery>;
export type GetRecommendedSoftwareVersionsLazyQueryHookResult = ReturnType<typeof useGetRecommendedSoftwareVersionsLazyQuery>;
export type GetRecommendedSoftwareVersionsQueryResult = Apollo.QueryResult<GetRecommendedSoftwareVersionsQuery, GetRecommendedSoftwareVersionsQueryVariables>;
export function refetchGetRecommendedSoftwareVersionsQuery(variables?: GetRecommendedSoftwareVersionsQueryVariables) {
return { query: GetRecommendedSoftwareVersionsDocument, variables: variables }
}
export const GetSoftwareVersionsDocument = gql`
query getSoftwareVersions($software: software_type_enum!) {
softwareVersions(where: {software: {_eq: $software}}, order_by: {version: desc}) {

View File

@@ -43,13 +43,13 @@ export default async function execPromiseWithErrorToast(
onError?.(error);
toast.custom(
const errorToastId = toast.custom(
(t) => (
<ErrorToast
isVisible={t.visible}
errorMessage={errorMessage}
error={error}
close={() => toast.dismiss()}
close={() => toast.dismiss(errorToastId)}
/>
),
{

View File

@@ -1,5 +1,31 @@
# @nhost/docs
## 2.14.2
### Patch Changes
- 86f3f8d: chore: fix broken link in react-native reference
## 2.14.1
### Patch Changes
- cf6b712: chore: add pnpm to list of supported packagers for functions
## 2.14.0
### Minor Changes
- 79ce7ca: feat: add react-native quickstart guide
- bedbb82: feat: functions: added runtime/pkg manager information
## 2.13.0
### Minor Changes
- 6fb0cc2: fix: minor improvements to compute resources' docs
- 66bd450: chore: various improvements
## 2.12.0
### Minor Changes

View File

@@ -0,0 +1,176 @@
---
title: Custom JWTs
description: Creating custom JWTs
icon: ghost
---
In some cases it is necessary to act on behalf of a user. While the Auth service doesn't allow that it is not difficult to implement such functionality as a serverless function. Below you can find an example of a function that can generate a valid access token for your application with customized values. For details read the docstring in the function itself.
Feel free to adapt to your needs.
### Dependencies
```
npm install jsonwebtoken
```
### Function
Create a file under `functions` (for instance `/functions/custom-jwts.ts`), with the following contents:
```js
import { Request, Response } from 'express'
import process from 'process'
import jwt from 'jsonwebtoken'
// function to extract jwt from the request
const getJwt = (req: Request): string | null => {
const authHeader = req.headers.authorization
if (!authHeader) {
return null
}
const parts = authHeader.split(' ')
if (parts.length !== 2) {
return null
}
const [scheme, token] = parts
if (!/^Bearer$/i.test(scheme)) {
return null
}
return token
}
// validate jwt token is valid and caller is either an admin or an operator
const jwtIsAuthorized = (req: Request, key: string): string => {
const token = getJwt(req)
if (!token) {
return ""
}
try {
const decoded = jwt.verify(token, key)
const claims = decoded['https://hasura.io/jwt/claims']
if ( !claims || !claims['x-hasura-allowed-roles'] ) {
return ""
}
if (
claims['x-hasura-allowed-roles'].includes('admin') ||
claims['x-hasura-allowed-roles'].includes('operator')
) {
return decoded.sub
}
return ""
} catch (e) {
return ""
}
}
/*
This is a sample function that generates a JWT token to impersonate users.
Authorization:
- send a valid JWT token with the request. The token should have the `admin` or `operator` role.
- send the `x-hasura-admin-secret` header with the request
Body:
A json object with the following keys:
- `userId` (string): the user id for which the token is generated
- `defaultRole` (string): the default role for the userId
- `allowedRoles` (array of strings): the roles that the userId can assume
Returns:
A json object with the following keys:
- `accessToken` (string) - The generated access token
In addition to the typical JWT claims generated by Nhost, the token generated by this function will have the following claims:
- `x-hasura-on-behalf-of`: the user id of the caller or `admin` if the caller used the `x-hasura-admin-secret` header
You are free to modify this function to suit your needs.
*/
export default (req: Request, res: Response) => {
let authorizedCaller = ""
if (req.headers['x-hasura-admin-secret'] === process.env.HASURA_GRAPHQL_ADMIN_SECRET) {
authorizedCaller = "admin"
}
const jwtSecret = JSON.parse(process.env.NHOST_JWT_SECRET)
if (!authorizedCaller) {
authorizedCaller = jwtIsAuthorized(req, jwtSecret.key)
}
if (!authorizedCaller) {
return res.status(401).json({ message: 'Unauthorized' })
}
// extract from json in the body
const {userId, defaultRole, allowedRoles} = req.body
if (!userId || !defaultRole || !allowedRoles) {
return res.status(400).json({ message: 'Bad request' })
}
let token = jwt.sign({
"exp": Math.floor(Date.now() / 1000) + 60 * 60, // 1 hour
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": allowedRoles,
"x-hasura-default-role": defaultRole,
"x-hasura-user-id": userId,
"x-hasura-user-is-anonymous": "false",
"x-hasura-on-behalf-of": authorizedCaller
},
"iat": Math.floor(Date.now() / 1000),
"iss": "custom-lambda",
"sub": userId,
}, jwtSecret.key);
res.status(200).json(
{
accessToken: token,
},
)
}
```
Now you can call it like:
```
curl -X POST \
-H "Content-Type: application/json" \
-H "x-hasura-admin-secret: nhost-admin-secret" \
-d '{"userId": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883", "defaultRole": "user", "allowedRoles": ["user", "me"]}' \
https://local.functions.nhost.run/v1/custom-jwt
{"accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTcxNDIyMTMsImh0dHBzOi8vaGFzdXJhLmlvL2p3dC9jbGFpbXMiOnsieC1oYXN1cmEtYWxsb3dlZC1yb2xlcyI6WyJ1c2VyIiwibWUiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoidXNlciIsIngtaGFzdXJhLXVzZXItaWQiOiJGRkFCNTM1NC1DNUVCLTQyQzEtOEJDMy1BRDIxRDIyOTc4ODMiLCJ4LWhhc3VyYS11c2VyLWlzLWFub255bW91cyI6ImZhbHNlIiwieC1oYXN1cmEtb24tYmVoYWxmLW9mIjoiYWRtaW4ifSwiaWF0IjoxNzE3MTM4NjEzLCJpc3MiOiJjdXN0b20tbGFtYmRhIiwic3ViIjoiRkZBQjUzNTQtQzVFQi00MkMxLThCQzMtQUQyMUQyMjk3ODgzIn0.bRhzJvXMdkQA8aXPH95uMT17WHED2rSRq3gE21Vp3Ak"}
```
The new token should be a valid token for your application with the custom values requested:
```json
{
"exp": 1717141288,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": [
"a",
"b"
],
"x-hasura-default-role": "user",
"x-hasura-user-id": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883",
"x-hasura-user-is-anonymous": "false",
"x-hasura-on-behalf-of": "admin"
},
"iat": 1717137688,
"iss": "custom-lambda",
"sub": "FFAB5354-C5EB-42C1-8BC3-AD21D2297883"
}
```

View File

@@ -93,3 +93,7 @@ nhost.auth.signIn({
provider: 'apple'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
</Note>

View File

@@ -41,3 +41,7 @@ nhost.auth.signIn({
provider: 'discord'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
</Note>

View File

@@ -64,3 +64,7 @@ nhost.auth.signIn({
provider: 'facebook'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
</Note>

View File

@@ -49,3 +49,7 @@ nhost.auth.signIn({
provider: "github",
});
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/custom-domains) documentation.
</Note>

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