Compare commits

..

21 Commits

Author SHA1 Message Date
github-actions[bot]
852f13b273 chore: update versions (#2824)
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.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/react-apollo@12.0.5

### Patch Changes

-   @nhost/apollo@7.1.5
-   @nhost/react@3.5.5

## @nhost/react-urql@9.0.5

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

- caa8bd7: fix: add error handling logic to transition to the signedOut
state when the token is invalid or expired

## @nhost/nextjs@2.1.19

### Patch Changes

-   @nhost/react@3.5.5

## @nhost/nhost-js@3.1.8

### Patch Changes

-   Updated dependencies [caa8bd7]
    -   @nhost/hasura-auth-js@2.5.5

## @nhost/react@3.5.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/vue@2.6.5

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost/dashboard@1.26.0

### Minor Changes

-   3773ad7: chore: update pricing information
- b63250d: fix: not allow run service creation form resubmission while
creating a run service
-   a44a1d4: feat: add rate limits settings page

### Patch Changes

-   @nhost/react-apollo@12.0.5
-   @nhost/nextjs@2.1.19

## @nhost/docs@2.15.0

### Minor Changes

-   40c0d7b: │feat: added subdomain/region information
-   a18b545: feat: added postgres upgrade docs

## @nhost-examples/cli@0.3.10

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-urql@9.0.5

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

## @nhost-examples/nextjs@0.3.10

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5
-   @nhost/nextjs@2.1.19

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/nhost-js@3.1.8

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/react@3.5.5

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

### Patch Changes

-   @nhost/react@3.5.5
-   @nhost/react-apollo@12.0.5

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

### Patch Changes

-   @nhost/nhost-js@3.1.8
-   @nhost/apollo@7.1.5
-   @nhost/vue@2.6.5

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

### Patch Changes

-   @nhost/apollo@7.1.5
-   @nhost/vue@2.6.5

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-27 10:48:43 +01:00
David BM
a44a1d48d6 feat (dashboard): change rate limits from dashboard (#2832) 2024-08-26 18:47:06 +02:00
David BM
b63250d1cb fix (dashboard): not allow run service creation form resubmission while creating a run service (#2838) 2024-08-23 15:20:27 +02:00
Hassan Ben Jobrane
caa8bd75ec fix(hasura-auth-js): transition to the signedOut state when the token is invalid or expired (#2835)
### **User description**
fixes https://github.com/nhost/nhost/issues/2817


___

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


___

### **Description**
- Added error handling logic to transition to the `signedOut` state when
the token is invalid or expired.
- Updated the authentication machine to handle 401 errors by signing out
the user.
- Enhanced test cases to verify the new behavior of signing out on
unauthorized errors.
- Updated Hasura page teardown logic to ensure the first matching
element is clicked.
- Added `micromatch` to the audit-ci allowlist for dependency
management.



___



### **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>Add error handling for
unauthorized token refresh</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

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

<li>Added error handling logic to transition to <code>signedOut</code>
state on <br>unauthorized error.<br> <li> Introduced a new condition
<code>isUnauthorizedError</code> to check for 401 <br>status.<br> <li>
Reordered imports for better organization.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Tests</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>refreshToken.test.ts</strong><dd><code>Update token
refresh test for unauthorized error handling</code></dd></summary>
<hr>

packages/hasura-auth-js/tests/refreshToken.test.ts

<li>Updated test to expect sign out on unauthorized error during token
<br>refresh.<br> <li> Adjusted test logic to match new authentication
state transitions.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>global-teardown.ts</strong><dd><code>Update Hasura
locator to click first matching element</code>&nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

dashboard/global-teardown.ts

<li>Updated locator to click the first matching element for Hasura page
<br>teardown.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>machine.typegen.ts</strong><dd><code>Update type
definitions for unauthorized error handling</code>&nbsp; &nbsp;
</dd></summary>
<hr>

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

- Added `isUnauthorizedError` to type definitions.



</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>silent-lies-smoke.md</strong><dd><code>Document bug fix
for invalid token handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/silent-lies-smoke.md

<li>Documented the bug fix for transitioning to <code>signedOut</code>
state on invalid <br>token.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>audit-ci.jsonc</strong><dd><code>Update audit-ci
allowlist with micromatch</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Added `micromatch` to the audit-ci allowlist.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2835/files#diff-4ede69da2a1704e53e08b8d647a315c202f037cc9277f16c94176d9622d261c6">+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-08-23 09:29:33 +01:00
David Barroso
40c0d7b914 feat (docs): added subdomain/region information (#2820) 2024-08-19 14:07:53 +02:00
David BM
3773ad7cca chore (dashboard): update pricing information (#2827)
Resolves #2822
2024-08-15 14:04:10 +02:00
Hassan Ben Jobrane
6f122521e9 fix: eval vulnerabilities (#2828)
### **PR Type**
enhancement, dependencies


___

### **Description**
- Removed `trim-newlines` from the `audit-ci` allowlist to address
potential vulnerabilities.
- Added `axios` version 1.7.4 to `package.json` resolutions to fix a
vulnerability.
- Updated `axios`, `@nhost/hasura-auth-js`, and `@nhost/nhost-js`
versions in `pnpm-lock.yaml` to ensure compatibility and security.



___



### **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>audit-ci.jsonc</strong><dd><code>Update audit-ci
allowlist configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Removed `trim-newlines` from the allowlist.



</details>


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

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

package.json

- Added `axios` version 1.7.4 to resolutions.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>pnpm-lock.yaml</strong><dd><code>Update dependencies in
pnpm-lock file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pnpm-lock.yaml

<li>Updated <code>axios</code> to version 1.7.4.<br> <li> Updated
<code>@nhost/hasura-auth-js</code> to version 2.5.4.<br> <li> Updated
<code>@nhost/nhost-js</code> to version 3.1.7.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2828/files#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bb">+11/-10</a>&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-08-15 11:39:09 +01:00
David Barroso
a18b545d2a feat (docs): added postgres upgrade docs (#2823) 2024-08-13 10:42:08 +02:00
github-actions[bot]
0263cc9e92 chore: update versions (#2804)
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.4

### Patch Changes

-   @nhost/nhost-js@3.1.7

## @nhost/react-apollo@12.0.4

### Patch Changes

-   @nhost/apollo@7.1.4
-   @nhost/react@3.5.4

## @nhost/react-urql@9.0.4

### Patch Changes

-   @nhost/react@3.5.4

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

### Patch Changes

-   4564232: chore: update `clientStorage` docs and add usage examples

## @nhost/nextjs@2.1.18

### Patch Changes

-   @nhost/react@3.5.4

## @nhost/nhost-js@3.1.7

### Patch Changes

-   Updated dependencies [4564232]
    -   @nhost/hasura-auth-js@2.5.4

## @nhost/react@3.5.4

### Patch Changes

-   @nhost/nhost-js@3.1.7

## @nhost/vue@2.6.4

### Patch Changes

-   @nhost/nhost-js@3.1.7

## @nhost/dashboard@1.25.0

### Minor Changes

- d1ceede: feat: add setting to migrate postgres major and/or minor
versions
- e5d3d1a: fix: allow manually typing column for custom check in
database row permissions

### Patch Changes

-   @nhost/react-apollo@12.0.4
-   @nhost/nextjs@2.1.18

## @nhost/docs@2.14.3

### Patch Changes

-   4564232: chore: update `clientStorage` docs and add usage examples

## @nhost-examples/cli@0.3.9

### Patch Changes

-   @nhost/nhost-js@3.1.7

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

### Patch Changes

-   @nhost/react@3.5.4
-   @nhost/react-apollo@12.0.4

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

### Patch Changes

-   @nhost/react@3.5.4

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

### Patch Changes

-   @nhost/react@3.5.4
-   @nhost/react-urql@9.0.4

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

### Patch Changes

-   @nhost/nhost-js@3.1.7

## @nhost-examples/nextjs@0.3.9

### Patch Changes

-   @nhost/react@3.5.4
-   @nhost/react-apollo@12.0.4
-   @nhost/nextjs@2.1.18

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

### Patch Changes

-   @nhost/nhost-js@3.1.7

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

### Patch Changes

-   @nhost/nhost-js@3.1.7

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

### Patch Changes

-   @nhost/react@3.5.4
-   @nhost/react-apollo@12.0.4

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

### Patch Changes

-   @nhost/react@3.5.4

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

### Patch Changes

-   @nhost/react@3.5.4
-   @nhost/react-apollo@12.0.4

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

### Patch Changes

-   @nhost/nhost-js@3.1.7
-   @nhost/apollo@7.1.4
-   @nhost/vue@2.6.4

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

### Patch Changes

-   @nhost/apollo@7.1.4
-   @nhost/vue@2.6.4

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-12 11:50:40 +01:00
David BM
d1ceedef05 feat (dashboard): UI for postgres migration (#2796)
Resolves #2748
2024-08-12 12:17:14 +02:00
Hassan Ben Jobrane
bdd84dd3ca chore: add e2e tests for run and ai pages (#2806)
### **User description**
resolves https://github.com/nhost/nhost/issues/2665


___

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


___

### **Description**
- Added e2e tests for creating and deleting run services, Assistants,
and Auto-Embeddings.
- Improved stability of PAT creation and deletion test by replacing
`waitForLoadState` with `waitForTimeout`.
- Added environment variables for pro test project in `env.ts`.
- Updated CI workflow to include `NHOST_PRO_TEST_PROJECT_NAME`
environment variable.
- Updated selectors and minor formatting changes for consistency.
- Addressed `fast-xml-parser` vulnerability by adding it to
dependencies.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>4
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>manage-pat.test.ts</strong><dd><code>Improve stability
of PAT creation and deletion test</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

dashboard/e2e/account/pat/manage-pat.test.ts

<li>Replaced <code>waitForLoadState</code> with
<code>waitForTimeout</code> for better stability.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>assistants.test.ts</strong><dd><code>Add e2e test for
Assistants management</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/e2e/ai/assistants.test.ts

<li>Added e2e test for creating and deleting Assistants.<br> <li>
Utilized <code>openProject</code> utility for navigation.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2806/files#diff-95533e004b514add57a2c87201a68cac11c20ffa458afd78e045ed89559e7546">+60/-0</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>auto-embeddings.test.ts</strong><dd><code>Add e2e test
for Auto-Embeddings management</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/e2e/ai/auto-embeddings.test.ts

<li>Added e2e test for creating and deleting Auto-Embeddings.<br> <li>
Utilized <code>openProject</code> utility for navigation.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>run.test.ts</strong><dd><code>Add e2e test for run
services management</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

dashboard/e2e/run/run.test.ts

<li>Added e2e test for creating and deleting run services.<br> <li>
Utilized <code>openProject</code> utility for navigation.<br>


</details>


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

</tr>                    
</table></details></td></tr><tr><td><strong>Configuration
changes</strong></td><td><details><summary>2 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>env.ts</strong><dd><code>Add environment variables for
pro test project</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/e2e/env.ts

- Added environment variables for pro test project.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ci.yaml</strong><dd><code>Update CI workflow with pro
test project variable</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

.github/workflows/ci.yaml

- Added `NHOST_PRO_TEST_PROJECT_NAME` environment variable.



</details>


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

</tr>                    
</table></details></td></tr><tr><td><strong>Bug
fix</strong></td><td><details><summary>1 files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>global-teardown.ts</strong><dd><code>Update SQL link
selector in global teardown</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/global-teardown.ts

- Updated selector for SQL link to use `data-test` attribute.



</details>


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

</tr>                    

</table></details></td></tr><tr><td><strong>Formatting</strong></td><td><details><summary>1
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>index.tsx</strong><dd><code>Minor formatting updates
for services page</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

dashboard/src/pages/[workspaceSlug]/[appSlug]/services/index.tsx

- Minor formatting changes for consistency.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2806/files#diff-e418070dbf550b4e92ae18e7f29bdd757d923b69e128cebe70f2fd3a47fcabd9">+11/-11</a>&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>package.json</strong><dd><code>Add fast-xml-parser
dependency</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>

package.json

- Added `fast-xml-parser` dependency.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>pnpm-lock.yaml</strong><dd><code>Update dependencies
and versions in lock file</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

pnpm-lock.yaml

<li>Updated dependencies and their versions.<br> <li> Added
<code>fast-xml-parser</code> dependency.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2806/files#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bb">+91/-75</a>&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-08-06 15:24:09 +01:00
Hassan Ben Jobrane
45642322f4 chore: update clientStorage documentation for Capacitor and add usage examples (#2799)
### **User description**
closes https://github.com/nhost/nhost/issues/2237


___

### **PR Type**
Documentation


___

### **Description**
- Enhanced `clientStorage` documentation across multiple files with
detailed usage examples.
- Added specific instructions for Capacitor versions < 4 and >= 4.
- Included new documentation file for `AuthOptions`.
- Added changesets for documentation updates.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Documentation</strong></td><td><details><summary>9
files</summary><table>
<tr>
  <td>
    <details>
<summary><strong>client.ts</strong><dd><code>Enhanced `clientStorage`
documentation with usage examples</code></dd></summary>
<hr>

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

<li>Added detailed usage examples for different
<code>clientStorageType</code> values.<br> <li> Included specific
instructions for Capacitor versions < 4 and >= 4.<br> <li> Updated
documentation for <code>react-native</code>, <code>capacitor</code>, and
<br><code>expo-secure-store</code>.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>five-avocados-complain.md</strong><dd><code>Added
changeset for `clientStorage` documentation update</code>&nbsp;
</dd></summary>
<hr>

.changeset/five-avocados-complain.md

- Added changeset for `@nhost/hasura-auth-js` with patch update.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>orange-pears-hug.md</strong><dd><code>Added changeset
for `clientStorage` documentation update</code>&nbsp; </dd></summary>
<hr>

.changeset/orange-pears-hug.md

- Added changeset for `@nhost/docs` with patch update.



</details>


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

</tr>                    

<tr>
  <td>
    <details>

<summary><strong>nhost-auth-constructor-params.mdx</strong><dd><code>Updated
`clientStorage` documentation with examples</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/reference/javascript/auth/types/nhost-auth-constructor-params.mdx

<li>Updated <code>clientStorage</code> documentation with detailed usage
examples.<br> <li> Included specific instructions for Capacitor versions
< 4 and >= 4.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2799/files#diff-38944ba6db61b7c7912f2ae68685c844ae6dedb355f525904dd4792dab758d45">+53/-4</a>&nbsp;
&nbsp; </td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>auth-options.mdx</strong><dd><code>Added documentation
for `AuthOptions` with examples</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></summary>
<hr>

docs/reference/javascript/nhost-js/types/auth-options.mdx

<li>Added new documentation file for <code>AuthOptions</code>.<br> <li>
Included detailed usage examples for different
<code>clientStorageType</code> <br>values.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>

<summary><strong>nhost-client-constructor-params.mdx</strong><dd><code>Updated
`clientStorage` documentation with examples</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


docs/reference/javascript/nhost-js/types/nhost-client-constructor-params.mdx

<li>Updated <code>clientStorage</code> documentation with detailed usage
examples.<br> <li> Included specific instructions for Capacitor versions
< 4 and >= 4.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>

<summary><strong>nhost-react-client-constructor-params.mdx</strong><dd><code>Updated
`clientStorage` documentation with examples</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/reference/nextjs/types/nhost-react-client-constructor-params.mdx

<li>Updated <code>clientStorage</code> documentation with detailed usage
examples.<br> <li> Included specific instructions for Capacitor versions
< 4 and >= 4.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>

<summary><strong>nhost-react-client-constructor-params.mdx</strong><dd><code>Updated
`clientStorage` documentation with examples</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/reference/react/types/nhost-react-client-constructor-params.mdx

<li>Updated <code>clientStorage</code> documentation with detailed usage
examples.<br> <li> Included specific instructions for Capacitor versions
< 4 and >= 4.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>

<summary><strong>nhost-vue-client-constructor-params.mdx</strong><dd><code>Updated
`clientStorage` documentation with examples</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

docs/reference/vue/types/nhost-vue-client-constructor-params.mdx

<li>Updated <code>clientStorage</code> documentation with detailed usage
examples.<br> <li> Included specific instructions for Capacitor versions
< 4 and >= 4.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2799/files#diff-c6d662d8a40177de1d661669c17bf0ced7385a5574dd4f2fe6159e815e697975">+53/-4</a>&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: Nuno Pato <nunopato@gmail.com>
2024-08-05 10:38:40 +01:00
Hassan Ben Jobrane
d092a7c395 chore: add "vue-template-compiler" to allowlist in audit-ci.jsonc (#2810)
### **PR Type**
enhancement


___

### **Description**
- Added `vue-template-compiler` to the `allowlist` in `audit-ci.jsonc`
to address vulnerabilities.



___



### **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>audit-ci.jsonc</strong><dd><code>Add
`vue-template-compiler` to audit-ci allowlist</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

audit-ci.jsonc

- Added `vue-template-compiler` to the `allowlist` array.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2810/files#diff-4ede69da2a1704e53e08b8d647a315c202f037cc9277f16c94176d9622d261c6">+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-26 17:20:29 +01:00
Zephyr (David B.M.)
e5d3d1a39f dashboard: fix: type custom row permissions autocomplete (#2757)
Fixes #2746
2024-07-17 18:53:09 +02:00
github-actions[bot]
f88bf2d034 chore: update versions (#2803)
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.24.1

### Patch Changes

- 49f2e55: fix: use service subdomain in service form and service
details dialog

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-17 00:35:14 +01:00
Hassan Ben Jobrane
49f2e55cb9 fix(dashboard): use service subdomain in service form and service details dialog (#2802)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Added `subdomain` prop to `ServiceDetailsDialog` component and its
interface.
- Updated `ServiceForm` to pass `subdomain` to `ServiceDetailsDialog`.
- Changed subdomain source from `currentProject` to `formValues` in
`PortsFormSection` URL generation.
- Added a changeset for the fix.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServiceForm.tsx</strong><dd><code>Pass subdomain to
ServiceDetailsDialog in ServiceForm</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/features/services/components/ServiceForm/ServiceForm.tsx

- Added `subdomain` prop to `ServiceDetailsDialog` component.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServiceDetailsDialog.tsx</strong><dd><code>Add and use
subdomain prop in ServiceDetailsDialog</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/ServiceDetailsDialog/ServiceDetailsDialog.tsx

<li>Added <code>subdomain</code> prop to
<code>ServiceDetailsDialogProps</code> interface.<br> <li> Updated
<code>getRunServicePortURL</code> call to use <code>subdomain</code>
prop.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Bug fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>PortsFormSection.tsx</strong><dd><code>Use formValues
subdomain in PortsFormSection URL generation</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSection.tsx

<li>Changed subdomain source from <code>currentProject</code> to
<code>formValues</code> in <br><code>getRunServicePortURL</code>
call.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>clever-hats-roll.md</strong><dd><code>Add changeset for
service subdomain fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>

.changeset/clever-hats-roll.md

- Added changeset for the fix.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2802/files#diff-ddf8c642ce16a0008ac12ebd0ab78740b6b0f35a356da5b0618bff2617ff3777">+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-17 00:21:12 +01:00
Hassan Ben Jobrane
598b988fc1 fix: use current project subdomain in ServiceDetailsDialog component (#2800)
### **PR Type**
Bug fix, Enhancement


___

### **Description**
- Removed the `subdomain` prop from the `ServiceDetailsDialog` component
and its usage in `ServiceForm`.
- Updated `ServiceDetailsDialog` to use `currentProject?.subdomain`
directly.
- Added a changeset file documenting the fix.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServiceForm.tsx</strong><dd><code>Remove `subdomain`
prop from `ServiceDetailsDialog` usage</code></dd></summary>
<hr>

dashboard/src/features/services/components/ServiceForm/ServiceForm.tsx

<li>Removed the <code>subdomain</code> prop from
<code>ServiceDetailsDialog</code> component.<br> <li> Updated the
<code>ServiceDetailsDialog</code> component usage.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Bug fix</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServiceDetailsDialog.tsx</strong><dd><code>Use
`currentProject?.subdomain` in
`ServiceDetailsDialog`</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/ServiceDetailsDialog/ServiceDetailsDialog.tsx

<li>Removed <code>subdomain</code> prop from
<code>ServiceDetailsDialogProps</code> interface.<br> <li> Updated
<code>ServiceDetailsDialog</code> to use
<code>currentProject?.subdomain</code> instead <br>of
<code>subdomain</code>.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>long-plums-shave.md</strong><dd><code>Add changeset for
`ServiceDetailsDialog` fix</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

.changeset/long-plums-shave.md

- Added changeset for the fix in `ServiceDetailsDialog` component.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2800/files#diff-8175ae1130dd45c62e1488253a620d11257d8fd83ec40740cf312171b976e226">+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-16 23:31:21 +01:00
github-actions[bot]
2f0910367d chore: update versions (#2794)
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.24.0

### Minor Changes

-   abb24af: chore: add redirect to support page when project is locked
- 18a6455: feat: show contact us info and locked reason when project is
locked

### Patch Changes

-   e31eefa: fix: include ingresses field when updating run services

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-15 15:51:18 +01:00
Hassan Ben Jobrane
e31eefae63 fix(dashboard): include ingresses field when updating a run service (#2798)
### **User description**
fixes https://github.com/nhost/nhost/issues/2797


___

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


___

### **Description**
- Added `ingresses` field to various components and validation schema to
support custom domains.
- Introduced `removeTypename` utility function to sanitize GraphQL
response objects.
- Replaced `getPortURL` with `getRunServicePortURL` helper function for
consistent URL generation.
- Updated changeset to document the inclusion of the `ingresses` field.



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>ServiceForm.tsx</strong><dd><code>Add ingresses field
and sanitize values in ServiceForm</code>&nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/features/services/components/ServiceForm/ServiceForm.tsx

<li>Added <code>removeTypename</code> utility function to sanitize
values.<br> <li> Included <code>ingresses</code> field in the ports
mapping.<br> <li> Updated health check and other fields to use sanitized
values.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServiceFormTypes.ts</strong><dd><code>Update validation
schema to include ingresses field</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/ServiceFormTypes.ts

<li>Added <code>ingresses</code> field to the validation schema.<br>
<li> Made <code>ingresses</code> field nullable.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>PortsFormSection.tsx</strong><dd><code>Use helper
function for port URL generation in
PortsFormSection</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSection.tsx

<li>Replaced <code>getPortURL</code> with
<code>getRunServicePortURL</code> helper function.<br> <li> Minor
formatting changes.<br>


</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-64ce17ad73e4122e8c66a1968b6737ec98bd1623ac7e3cd3f4a34b549a78717b">+10/-13</a>&nbsp;
</td>

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServiceDetailsDialog.tsx</strong><dd><code>Use helper
function for port URL generation in
ServiceDetailsDialog</code></dd></summary>
<hr>


dashboard/src/features/services/components/ServiceForm/components/ServiceDetailsDialog/ServiceDetailsDialog.tsx

<li>Replaced <code>getPortURL</code> with
<code>getRunServicePortURL</code> helper function.<br> <li> Filtered and
displayed only published ports.<br>


</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>ServicesList.tsx</strong><dd><code>Include ingresses
field in ServicesList ports mapping</code>&nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

dashboard/src/features/services/components/ServicesList/ServicesList.tsx

- Included `ingresses` field in the ports mapping.



</details>


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

</tr>                    

<tr>
  <td>
    <details>
<summary><strong>helpers.ts</strong><dd><code>Add helper functions for
port URL generation and typename removal</code></dd></summary>
<hr>

dashboard/src/utils/helpers/helpers.ts

<li>Added <code>getRunServicePortURL</code> helper function.<br> <li>
Enhanced <code>removeTypename</code> function.<br>


</details>


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

</tr>                    
</table></td></tr><tr><td><strong>Documentation</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>short-radios-retire.md</strong><dd><code>Add changeset
for ingresses field inclusion</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></summary>
<hr>

.changeset/short-radios-retire.md

- Added changeset for including `ingresses` field in run services.



</details>


  </td>
<td><a
href="https://github.com/nhost/nhost/pull/2798/files#diff-f738014a2859f7ce7160422ab65bfaffd0d81f8e603a46febb468ac05f6087c0">+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-15 15:38:04 +01:00
Zephyr (David B.M.)
abb24afad5 chore (dashboard): locked project contact support redirect (#2795) 2024-07-09 20:25:16 +02:00
Zephyr (David B.M.)
18a64555ce feat (dashboard): show contact us info when project is locked (#2775)
Resolves #2624
2024-07-09 15:11:58 +02:00
153 changed files with 4624 additions and 502 deletions

View File

@@ -22,6 +22,7 @@ env:
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
NHOST_PRO_TEST_PROJECT_NAME: ${{ vars.NHOST_PRO_TEST_PROJECT_NAME }}
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}

View File

@@ -2,5 +2,5 @@
// $schema provides code completion hints to IDEs.
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true,
"allowlist": ["trim-newlines"]
"allowlist": ["vue-template-compiler", "micromatch"]
}

View File

@@ -1,5 +1,48 @@
# @nhost/dashboard
## 1.26.0
### Minor Changes
- 3773ad7: chore: update pricing information
- b63250d: fix: not allow run service creation form resubmission while creating a run service
- a44a1d4: feat: add rate limits settings page
### Patch Changes
- @nhost/react-apollo@12.0.5
- @nhost/nextjs@2.1.19
## 1.25.0
### Minor Changes
- d1ceede: feat: add setting to migrate postgres major and/or minor versions
- e5d3d1a: fix: allow manually typing column for custom check in database row permissions
### Patch Changes
- @nhost/react-apollo@12.0.4
- @nhost/nextjs@2.1.18
## 1.24.1
### Patch Changes
- 49f2e55: fix: use service subdomain in service form and service details dialog
- 598b988: fix: use current project subdomain in ServiceDetailsDialog component
## 1.24.0
### Minor Changes
- abb24af: chore: add redirect to support page when project is locked
- 18a6455: feat: show contact us info and locked reason when project is locked
### Patch Changes
- e31eefa: fix: include ingresses field when updating run services
## 1.23.0
### Minor Changes

View File

@@ -17,7 +17,7 @@ test.afterAll(async () => {
});
test('should be able to create then delete a personal access token', async () => {
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.getByRole('banner').getByRole('button').last().click();
await page.getByRole('link', { name: /account settings/i }).click();
await page

View File

@@ -0,0 +1,60 @@
import {
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /ai/i })
.click();
});
test.afterAll(async () => {
await page.close();
});
test('should create and delete an Assistant', async () => {
await page.getByRole('link', { name: 'Assistants' }).click();
await expect(page.getByText(/no assistants are configured/i)).toBeVisible();
await page.getByRole('button', { name: 'Create a new assistant' }).click();
await page.getByLabel('Name').fill('test');
await page.getByLabel('Description').fill('test');
await page.getByLabel('Instructions').fill('test');
await page.getByLabel('Model').fill('gpt-3.5-turbo-1106');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
await page.getByLabel(/more options/i).click();
await page.getByRole('menuitem', { name: /delete test/i }).click();
await page.getByLabel('Confirm Delete Assistant').check();
await page.getByRole('button', { name: 'Delete Assistant' }).click();
await expect(
page.getByRole('heading', { name: /no assistants are configured/i }),
).toBeVisible();
});

View File

@@ -0,0 +1,55 @@
import {
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /ai/i })
.click();
});
test.afterAll(async () => {
await page.close();
});
test('should create and delete an Auto-Embeddings', async () => {
await page.getByRole('button', { name: 'Add a new Auto-Embeddings' }).click();
await page.getByLabel('Name').fill('test');
await page.getByLabel('Schema').fill('auth');
await page.getByLabel('Table').fill('users');
await page.getByLabel('Column').fill('email');
await page.getByRole('button', { name: 'Create' }).click();
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
await page.getByLabel(/more options/i).click();
await page.getByRole('menuitem', { name: /delete test/i }).click();
await page.getByLabel('Confirm Delete Auto-').check();
await page.getByRole('button', { name: 'Delete Auto-Embeddings' }).click();
await expect(
page.getByRole('heading', { name: /No Auto-Embeddings are configured/i }),
).toBeVisible();
});

View File

@@ -23,6 +23,11 @@ export const TEST_WORKSPACE_SLUG = slugify(TEST_WORKSPACE_NAME, {
*/
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
/**
* Name of the pro test project to test against.
*/
export const PRO_TEST_PROJECT_NAME = process.env.NHOST_PRO_TEST_PROJECT_NAME;
/**
* Slugified name of the project to test against.
*/
@@ -31,6 +36,14 @@ export const TEST_PROJECT_SLUG = slugify(TEST_PROJECT_NAME, {
strict: true,
});
/**
* Slugified name of the pro project to test against.
*/
export const PRO_TEST_PROJECT_SLUG = slugify(PRO_TEST_PROJECT_NAME, {
lower: true,
strict: true,
});
/**
* Hasura admin secret of the test project to use.
*/

View File

@@ -0,0 +1,95 @@
import {
PRO_TEST_PROJECT_NAME,
PRO_TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: PRO_TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: PRO_TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /run/i })
.click();
});
test.afterAll(async () => {
await page.close();
});
test('should create and delete a run service', async () => {
await page.getByRole('button', { name: 'Add service' }).first().click();
await expect(page.getByText(/create a new service/i)).toBeVisible();
await page.getByPlaceholder(/service name/i).click();
await page.getByPlaceholder(/service name/i).fill('test');
const sliderRail = page.locator(
'.space-y-4 > .MuiSlider-root > .MuiSlider-rail',
);
// Get the bounding box of the slider rail to determine where to click
const box = await sliderRail.boundingBox();
if (box) {
// Calculate the position to click (start of the rail)
const x = box.x + 1; // A little offset to ensure click inside the rail
const y = box.y + box.height / 2; // Middle of the rail height-wise
// Perform the click
await page.mouse.click(x, y);
}
await page.getByRole('button', { name: /create/i }).click();
await expect(
page.getByRole('heading', { name: /confirm resources/i }),
).toBeVisible();
await page.waitForTimeout(1000);
await page.getByRole('button', { name: /confirm/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /service details/i }),
).toBeVisible();
await page.getByRole('button', { name: /ok/i }).click();
await expect(page.getByRole('heading', { name: /test/i })).toBeVisible();
await page.getByLabel(/more options/i).click();
await page.getByRole('menuitem', { name: /delete service/i }).click();
await page.getByLabel(/confirm delete project #/i).check();
await page
.getByText(/delete service/i)
.nth(2)
.click();
await page.getByLabel('Close').click();
await expect(
page
.getByRole('main')
.locator('div')
.filter({ hasText: 'No custom services are' })
.nth(2),
).toBeVisible();
});

View File

@@ -43,8 +43,8 @@ async function globalTeardown() {
await adminSecretInput.press('Enter');
// note: getByRole doesn't work here
await hasuraPage.locator('a', { hasText: /data/i }).click();
await hasuraPage.getByRole('link', { name: /sql/i }).click();
await hasuraPage.locator('a', { hasText: /data/i }).nth(0).click();
await hasuraPage.locator('[data-test="sql-link"]').click();
// Set the value of the Ace code editor using JavaScript evaluation in the browser context
await hasuraPage.evaluate(() => {

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.23.0",
"version": "1.26.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -0,0 +1,12 @@
<svg width="72" height="73" viewBox="0 0 72 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10501_253)">
<path d="M0 8.5C0 4.08172 3.58172 0.5 8 0.5H64C68.4183 0.5 72 4.08172 72 8.5V64.5C72 68.9183 68.4183 72.5 64 72.5H8C3.58172 72.5 0 68.9183 0 64.5V8.5Z" fill="#9C73DF" fill-opacity="0.2"/>
<path d="M43.1203 35.5H29.7687C28.7153 35.5 27.8613 36.3954 27.8613 37.5V44.5C27.8613 45.6046 28.7153 46.5 29.7687 46.5H43.1203C44.1737 46.5 45.0276 45.6046 45.0276 44.5V37.5C45.0276 36.3954 44.1737 35.5 43.1203 35.5Z" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M31.6758 35.5V31.5C31.6758 30.1739 32.1782 28.9021 33.0724 27.9645C33.9667 27.0268 35.1795 26.5 36.4442 26.5C37.7089 26.5 38.9217 27.0268 39.816 27.9645C40.7102 28.9021 41.2126 30.1739 41.2126 31.5V35.5" stroke="#9C73DF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_10501_253">
<rect width="72" height="72" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -38,9 +38,12 @@ export default function Header({ className, ...props }: HeaderProps) {
const isProjectUpdating =
currentProject?.appStates[0]?.stateId === ApplicationStatus.Updating;
const isProjectMigratingDatabase =
currentProject?.appStates[0]?.stateId === ApplicationStatus.Migrating;
// Poll for project updates
useEffect(() => {
if (!isProjectUpdating) {
if (!isProjectUpdating && !isProjectMigratingDatabase) {
return () => {};
}
@@ -51,7 +54,7 @@ export default function Header({ className, ...props }: HeaderProps) {
return () => {
clearInterval(interval);
};
}, [isProjectUpdating, refetchProject]);
}, [isProjectUpdating, isProjectMigratingDatabase, refetchProject]);
const openDevAssistant = () => {
// The dev assistant can be only answer questions related to a particular project
@@ -92,6 +95,13 @@ export default function Header({ className, ...props }: HeaderProps) {
{isProjectUpdating && (
<Chip size="small" label="Updating" color="warning" />
)}
{isProjectMigratingDatabase && (
<Chip
size="small"
label="Upgrading Postgres version"
color="warning"
/>
)}
</div>
<div className="hidden grid-flow-col items-center gap-2 sm:grid">

View File

@@ -63,6 +63,10 @@ export interface SettingsContainerProps
* @default false
*/
showSwitch?: boolean;
/**
* Custom element to be rendered at the top-right corner of the section.
*/
topRightElement?: ReactNode;
/**
* Custom class names passed to the root element.
*/
@@ -108,6 +112,7 @@ export default function SettingsContainer({
showSwitch = false,
rootClassName,
docsTitle,
topRightElement,
slotProps: { root, switch: switchSlot, submitButton, footer } = {},
}: SettingsContainerProps) {
return (
@@ -137,6 +142,7 @@ export default function SettingsContainer({
{description && <Text color="secondary">{description}</Text>}
</div>
</div>
{topRightElement}
{!switchId && showSwitch && (
<Switch
checked={enabled}

View File

@@ -221,6 +221,13 @@ export default function SettingsSidebar({
>
Custom Domains
</SettingsNavLink>
<SettingsNavLink
href="/rate-limiting"
exact={false}
onClick={handleSelect}
>
Rate Limiting
</SettingsNavLink>
<SettingsNavLink href="/ai" exact={false} onClick={handleSelect}>
AI
</SettingsNavLink>

View File

@@ -8,6 +8,17 @@ import { createTheme as createMuiTheme } from '@mui/material/styles';
* @param mode - Color mode
* @returns Material UI theme
*/
declare module '@mui/material/styles' {
interface Palette {
beige: Palette['primary'];
}
interface PaletteOptions {
beige?: PaletteOptions['primary'];
}
}
export default function createTheme(mode: PaletteMode) {
return createMuiTheme({
shape: {

View File

@@ -0,0 +1,27 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function PowerOffIcon(props: IconProps) {
return (
<SvgIcon
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
{...props}
>
<path
fill="none"
d="M18.36 6.64A9 9 0 0 1 20.77 15M6.16 6.16a9 9 0 1 0 12.68 12.68M12 2v4M2 2l20 20"
/>
</SvgIcon>
);
}
PowerOffIcon.displayName = 'NhostPowerOffIcon';
export default PowerOffIcon;

View File

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

View File

@@ -0,0 +1,31 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function RepeatIcon(props: IconProps) {
return (
<SvgIcon
aria-label="Repeat"
width="16"
height="20"
viewBox="0 0 16 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11.4062 11.9779H15.9998L13.7035 8L11.4062 11.9779Z"
fill="currentColor"
/>
<path
d="M13.1959 16.2243C13.1959 17.466 11.9655 18.4759 10.4525 18.4759L4.05328 18.4749C2.54037 18.4749 1.30989 17.2444 1.30989 15.7315V4.26843C1.30989 2.75552 2.54034 1.52504 4.05328 1.52504H10.4525C11.9654 1.52504 13.1959 2.535 13.1959 3.77661V6.53613C13.1959 6.81655 13.4235 7.04415 13.7039 7.04415C13.9844 7.04415 14.212 6.81655 14.212 6.53613V3.77557C14.212 1.97409 12.5253 0.508057 10.4526 0.508057L4.05333 0.509073C1.98056 0.509073 0.293945 2.19574 0.293945 4.26846V15.7326C0.293945 17.8054 1.98061 19.492 4.05333 19.492H10.4526C12.5253 19.492 14.212 18.0258 14.212 16.2245L14.212 13.5H13.1959L13.1959 16.2243Z"
fill="currentColor"
stroke="currentColor"
strokeWidth="0.5"
/>
</SvgIcon>
);
}
RepeatIcon.displayName = 'NhostRepeatIcon';
export default RepeatIcon;

View File

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

View File

@@ -63,6 +63,9 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
paper: '#171d26',
},
divider: '#2f363d',
beige: {
main: '#362c22',
},
};
}
@@ -125,5 +128,8 @@ export default function getDesignTokens(mode: PaletteMode): PaletteOptions {
paper: '#ffffff',
},
divider: '#eaedf0',
beige: {
main: '#e5d1bf',
},
};
}

View File

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

View File

@@ -0,0 +1,93 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useGetApplicationBackupsQuery,
type GetApplicationBackupsQuery,
type GetApplicationBackupsQueryVariables,
} from '@/utils/__generated__/graphql';
import type { QueryHookOptions } from '@apollo/client';
interface TimePeriod {
value: number;
unit: 'hours' | 'minutes';
downtime: string;
downtimeShort: string;
}
export interface UseEstimatedDatabaseMigrationDowntimeOptions
extends QueryHookOptions<
GetApplicationBackupsQuery,
GetApplicationBackupsQueryVariables
> {}
const DEFAULT_ESTIMATED_DOWNTIME: TimePeriod = {
value: 10,
unit: 'minutes',
downtime: '10 minutes',
downtimeShort: '10min',
};
function getEstimatedTime(diff: number): TimePeriod {
if (diff > 1000 * 3600) {
const value = Math.floor(diff / (1000 * 3600));
const unitStr = value === 1 ? 'hour' : 'hours';
return {
value,
unit: 'hours',
downtime: `${value} ${unitStr}`,
downtimeShort: `${value}hr`,
};
}
// 10 minutes is the minimum estimated downtime
if (diff > 1000 * 60 * 10) {
const value = Math.floor(diff / (1000 * 60));
const unitStr = value === 1 ? 'minute' : 'minutes';
return {
value,
unit: 'minutes',
downtime: `${value} ${unitStr}`,
downtimeShort: `${value}min`,
};
}
return DEFAULT_ESTIMATED_DOWNTIME;
}
/*
* This hook returns the estimated downtime for a database migration.
* The estimated downtime is calculated based on the time taken to complete the last backup.
* If there are no backups, the estimated downtime is set to 10 minutes.
*/
export default function useEstimatedDatabaseMigrationDowntime(
options: UseEstimatedDatabaseMigrationDowntimeOptions = {},
): TimePeriod {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject?.plan?.isFree;
const { data, loading, error } = useGetApplicationBackupsQuery({
...options,
variables: { ...options.variables, appId: currentProject?.id },
skip: isPlanFree,
});
if (loading || error) {
return DEFAULT_ESTIMATED_DOWNTIME;
}
const backups = data?.app?.backups;
let estimatedMilliseconds = 1000 * 60 * 10; // DEFAULT ESTIMATED DOWNTIME is 10 minutes
if (!isPlanFree && backups?.length > 0) {
const lastBackup = backups[0];
const createdAt = new Date(lastBackup.createdAt);
const completedAt = new Date(lastBackup.completedAt);
const diff = completedAt.valueOf() - createdAt.valueOf();
estimatedMilliseconds = diff * 2;
}
const estimated = getEstimatedTime(estimatedMilliseconds);
return estimated;
}

View File

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

View File

@@ -0,0 +1,37 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { useGetPostgresSettingsQuery } from '@/utils/__generated__/graphql';
/**
* Queries the postgres version of the current project.
* @returns Major, minor and full version of the postgres database. Loading and error states.
*/
export default function useGetPostgresVersion() {
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const isPlatform = useIsPlatform();
const {
data: postgresSettingsData,
loading,
error,
} = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { version } = postgresSettingsData?.config?.postgres || {};
const [postgresMajor, postgresMinor] = version?.split('.') || [
undefined,
undefined,
];
return {
version,
postgresMajor,
postgresMinor,
loading,
error,
};
}

View File

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

View File

@@ -0,0 +1,98 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useGetApplicationStateQuery,
type GetApplicationStateQuery,
type GetApplicationStateQueryVariables,
} from '@/generated/graphql';
import { ApplicationStatus } from '@/types/application';
import type { QueryHookOptions } from '@apollo/client';
import { useVisibilityChange } from '@uidotdev/usehooks';
import { useEffect } from 'react';
export interface UseIsDatabaseMigratingOptions
extends QueryHookOptions<
GetApplicationStateQuery,
GetApplicationStateQueryVariables
> {
shouldPoll?: boolean;
}
/*
* This hook returns information about the current state of database migration.
* @param options - Options for the query.
*
* @returns - An object with two properties:
* - isMigrating: true if the database is currently migrating.
* - shouldShowUpgradeLogs: true if the database is currently migrating or the application is not live after a migration.
*/
export default function useIsDatabaseMigrating(
options: UseIsDatabaseMigratingOptions = {},
): {
isMigrating: boolean;
shouldShowUpgradeLogs: boolean;
} {
const { currentProject } = useCurrentWorkspaceAndProject();
const isVisible = useVisibilityChange();
const {
data: appStatesData,
startPolling,
stopPolling,
} = useGetApplicationStateQuery({
...options,
variables: { ...options.variables, appId: currentProject?.id },
skip: !currentProject,
skipPollAttempt: () => !isVisible,
});
useEffect(() => {
if (options.shouldPoll) {
startPolling(options.pollInterval || 5000);
}
return () => stopPolling();
}, [stopPolling, startPolling, options.shouldPoll, options.pollInterval]);
// Return true if the application is migrating or if the application is not live after a migration
const shouldShowUpgradeLogs = (
appStates: GetApplicationStateQuery['app']['appStates'],
) => {
for (let i = 0; i < appStates.length; i += 1) {
if (appStates[i].stateId === ApplicationStatus.Live) {
return false;
}
if (appStates[i].stateId === ApplicationStatus.Migrating) {
return true;
}
}
return false;
};
// Return true if the application is currently migrating
const isMigrating = (
appStates: GetApplicationStateQuery['app']['appStates'],
) => {
for (let i = 0; i < appStates.length; i += 1) {
if (appStates[i].stateId === ApplicationStatus.Live) {
return false;
}
if (appStates[i].stateId === ApplicationStatus.Errored) {
return false;
}
if (appStates[i].stateId === ApplicationStatus.Migrating) {
return true;
}
}
return false;
};
return {
isMigrating: isMigrating(appStatesData?.app?.appStates || []),
shouldShowUpgradeLogs: shouldShowUpgradeLogs(
appStatesData?.app?.appStates || [],
),
};
}

View File

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

View File

@@ -0,0 +1,107 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useGetApplicationStateQuery,
useGetSystemLogsQuery,
type GetApplicationStateQuery,
type GetApplicationStateQueryVariables,
type GetSystemLogsQuery,
type GetSystemLogsQueryVariables,
} from '@/generated/graphql';
import { ApplicationStatus } from '@/types/application';
import type { ApolloError, QueryHookOptions } from '@apollo/client';
import { useVisibilityChange } from '@uidotdev/usehooks';
import { useEffect } from 'react';
export interface UseIsDatabaseMigratingOptions
extends QueryHookOptions<
GetApplicationStateQuery,
GetApplicationStateQueryVariables
> {
shouldPoll?: boolean;
}
export interface UseMigrationLogsOptions
extends QueryHookOptions<GetSystemLogsQuery, GetSystemLogsQueryVariables> {
shouldPoll?: boolean;
}
export interface Log {
level: string;
msg: string;
time: string;
}
/*
* Returns logs for the current database migration.
* @param options - Options for the getSystemLogs query.
* @returns - An object with three properties:
* - logs: Logs for the current/latest database migration.
* - loading: true if the getLogs query is in a loading state.
* - error: Error object if the query failed.
*/
export default function useMigrationLogs(
options: UseMigrationLogsOptions = {},
): {
logs: Partial<Log>[];
loading: boolean;
error: ApolloError;
} {
const { currentProject } = useCurrentWorkspaceAndProject();
const isVisible = useVisibilityChange();
const { data: appStatesData } = useGetApplicationStateQuery({
variables: { appId: currentProject?.id },
skip: !currentProject,
});
const migrationStartTimestamp = appStatesData?.app?.appStates?.find(
(state) => state.stateId === ApplicationStatus.Migrating,
)?.createdAt;
const from = new Date(migrationStartTimestamp);
const { data, loading, error, startPolling, stopPolling } =
useGetSystemLogsQuery({
...options,
variables: {
...options.variables,
appID: currentProject.id,
action: 'change-database-version',
from,
},
skip: !currentProject || !from,
skipPollAttempt: () => !isVisible,
});
useEffect(() => {
if (options.shouldPoll) {
startPolling(options.pollInterval || 5000);
}
return () => stopPolling();
}, [stopPolling, startPolling, options.shouldPoll, options.pollInterval]);
const systemLogs = data?.systemLogs ?? [];
const sortedLogs = [...systemLogs];
sortedLogs.sort(
(a, b) => new Date(a.timestamp).valueOf() - new Date(b.timestamp).valueOf(),
); // sort in ascending order
const logs = sortedLogs.map(({ log }) => {
let logObj: Partial<Log> = {};
try {
logObj = JSON.parse(log);
return logObj;
} catch (e) {
console.error('Failed to parse log', log);
return undefined;
}
});
return {
logs,
loading,
error,
};
}

View File

@@ -19,6 +19,7 @@ import { useAutocomplete } from '@mui/base/useAutocomplete';
import type { AutocompleteRenderGroupParams } from '@mui/material/Autocomplete';
import { autocompleteClasses } from '@mui/material/Autocomplete';
import type {
ChangeEvent,
ForwardedRef,
HTMLAttributes,
PropsWithoutRef,
@@ -209,6 +210,7 @@ function ColumnAutocomplete(
]);
}
const options = useColumnGroups({
selectedSchema,
selectedTable,
@@ -241,6 +243,33 @@ function ColumnAutocomplete(
onChange: handleChange,
});
function handleInputValueChange(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
const {value} = event.target
setInputValue(value)
setSelectedColumn(
{
value,
label: value,
metadata: selectedColumn?.metadata || {
table_schema: selectedSchema,
table_name: selectedTable,
}
});
onChange?.(event, {
value:
selectedRelationships.length > 0
? [relationshipDotNotation, value].join('.')
: value,
columnMetadata: {
table_schema: selectedSchema,
table_name: selectedTable,
},
});
}
return (
<>
<div {...getRootProps()} className={rootClassName}>
@@ -293,7 +322,7 @@ function ColumnAutocomplete(
helperText={
String(tableError || metadataError || '') || props.helperText
}
onChange={(event) => setInputValue(event.target.value)}
onChange={handleInputValueChange}
value={inputValue}
startAdornment={
selectedColumn || relationshipDotNotation ? (
@@ -305,7 +334,7 @@ function ColumnAutocomplete(
className="!ml-2 flex-shrink-0 truncate lg:max-w-[200px]"
>
<Text component="span" color="disabled">
{defaultTable}
{selectedTable}
</Text>
.
{relationshipDotNotation && (

View File

@@ -0,0 +1,27 @@
import { Alert } from '@/components/ui/v2/Alert';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { Text } from '@/components/ui/v2/Text';
export default function DatabaseMigrateWarning() {
return (
<Alert severity="error" className="flex flex-col gap-3 text-left">
<Text
className="flex items-center gap-1 font-semibold"
sx={{
color: 'error.main',
}}
>
<XIcon className="h-4 w-4" /> Error: Database version upgrade not
possible
</Text>
<Text
sx={{
color: 'error.main',
}}
>
Your project isn&apos;t currently in a healthy state. Please, review
before proceeding with the upgrade.
</Text>
</Alert>
);
}

View File

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

View File

@@ -0,0 +1,38 @@
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { useEstimatedDatabaseMigrationDowntime } from '@/features/database/common/hooks/useEstimatedDatabaseMigrationDowntime';
export default function DatabaseMigrateDowntimeWarning() {
const { downtimeShort } = useEstimatedDatabaseMigrationDowntime();
return (
<Alert severity="warning" className="flex flex-col gap-3 text-left">
<div className="flex flex-col gap-2 lg:flex-row lg:justify-between">
<Text className="flex items-start gap-1 font-semibold">
<span></span> Warning: upgrading Postgres major version
</Text>
<div className="flex">
<Box
sx={{
backgroundColor: 'beige.main',
}}
className="py-1/2 flex items-center justify-center text-nowrap rounded-full px-2 font-semibold"
>
Estimated downtime ~{downtimeShort}
</Box>
</div>
</div>
<div className="flex flex-col gap-4">
<Text>
Upgrading a major version of Postgres requires downtime. The amount of
downtime will depend on your database size, so plan ahead in order to
reduce the impact on your users.
</Text>
<Text>
Note that it isn&apos;t possible to downgrade between major versions.
</Text>
</div>
</Alert>
);
}

View File

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

View File

@@ -0,0 +1,112 @@
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { useMigrationLogs } from '@/features/database/common/hooks/useMigrationLogs';
export default function DatabaseMigrateLogsModal() {
const { logs, loading, error } = useMigrationLogs({
shouldPoll: true,
});
if (error) {
return (
<Box className="pt-2">
<Box
className="min-h-80 p-4"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.300' : 'grey.700',
}}
>
<Text
className="font-mono"
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
}}
>
Could not fetch logs. Error: {error.message}
</Text>
</Box>
</Box>
);
}
if (loading) {
return (
<Box className="pt-2">
<Box
className="min-h-80 p-4"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.300' : 'grey.700',
}}
>
<Text
className="font-mono"
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
}}
>
Loading...
</Text>
</Box>
</Box>
);
}
if (logs.length === 0) {
return (
<Box className="pt-2">
<Box
className="min-h-80 p-4"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.300' : 'grey.700',
}}
>
<Text
className="font-mono"
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
}}
>
No logs found
</Text>
</Box>
</Box>
);
}
return (
<Box className="pt-2">
<Box
className="min-h-80 p-4"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'dark' ? 'grey.300' : 'grey.700',
}}
>
{logs.map((logObj) => {
if (logObj?.level && logObj?.msg) {
return (
<Text
key={`${logObj.msg}${logObj.time}`}
className="font-mono"
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
}}
>
{logObj.level.toUpperCase()}: {logObj.msg}
</Text>
);
}
return undefined;
})}
</Box>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,121 @@
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 { useEstimatedDatabaseMigrationDowntime } from '@/features/database/common/hooks/useEstimatedDatabaseMigrationDowntime';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetPostgresSettingsDocument,
GetWorkspaceAndProjectDocument,
useUpdateDatabaseVersionMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface DatabaseMigrateVersionConfirmationDialogProps {
/**
* Function to be called when the user clicks the cancel button.
*/
onCancel: () => void;
/**
* Function to be called when the user clicks the proceed button.
*/
onProceed: () => void;
/**
* New version to migrate to.
*/
postgresVersion: string;
}
export default function DatabaseMigrateVersionConfirmationDialog({
onCancel,
onProceed,
postgresVersion,
}: DatabaseMigrateVersionConfirmationDialogProps) {
const isPlatform = useIsPlatform();
const { openDialog, closeDialog } = useDialog();
const localMimirClient = useLocalMimirClient();
const [loading, setLoading] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updatePostgresMajor] = useUpdateDatabaseVersionMutation({
refetchQueries: [
GetPostgresSettingsDocument,
GetWorkspaceAndProjectDocument,
],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { downtime } = useEstimatedDatabaseMigrationDowntime({
fetchPolicy: 'cache-only',
});
async function handleClick() {
setLoading(true);
await execPromiseWithErrorToast(
async () => {
await updatePostgresMajor({
variables: {
appId: currentProject.id,
version: postgresVersion,
},
});
onProceed();
closeDialog();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating postgres version...',
successMessage: 'Major version upgrade started.',
errorMessage:
'An error occurred while updating the database version. Please try again later.',
},
);
}
return (
<Box className={twMerge('w-full rounded-lg p-6 pt-0 text-left')}>
<div className="grid grid-flow-row gap-6">
<Text>
The upgrade process will require an{' '}
<span className="font-semibold">
estimated {downtime} of downtime
</span>
. To continue with the upgrade process, click on &quot;Proceed&quot;.
</Text>
<div className="grid grid-flow-col gap-4">
<Button
variant="outlined"
color="secondary"
onClick={() => {
onCancel();
closeDialog();
}}
>
Cancel
</Button>
<Button onClick={handleClick} loading={loading}>
Proceed
</Button>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -5,28 +5,46 @@ import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { RepeatIcon } from '@/components/ui/v2/icons/RepeatIcon';
import { useGetPostgresVersion } from '@/features/database/common/hooks/useGetPostgresVersion';
import { useIsDatabaseMigrating } from '@/features/database/common/hooks/useIsDatabaseMigrating';
import { DatabaseMigrateDisabledError } from '@/features/database/settings/components/DatabaseMigrateDisabledError';
import { DatabaseMigrateDowntimeWarning } from '@/features/database/settings/components/DatabaseMigrateDowntimeWarning';
import { DatabaseMigrateLogsModal } from '@/features/database/settings/components/DatabaseMigrateLogsModal';
import { DatabaseMigrateVersionConfirmationDialog } from '@/features/database/settings/components/DatabaseMigrateVersionConfirmationDialog';
import { DatabaseUpdateInProgressWarning } from '@/features/database/settings/components/DatabaseUpdateInProgressWarning';
import { useAppState } from '@/features/projects/common/hooks/useAppState';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetPostgresSettingsDocument,
GetWorkspaceAndProjectDocument,
Software_Type_Enum,
useGetPostgresSettingsQuery,
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { ApplicationStatus } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
version: Yup.object({
majorVersion: Yup.object({
label: Yup.string().required(),
value: Yup.string().required(),
value: Yup.string().required('Major version is a required field'),
})
.label('Postgres Version')
.label('Postgres major version')
.required(),
minorVersion: Yup.object({
label: Yup.string().required(),
value: Yup.string().required('Minor version is a required field'),
})
.label('Postgres minor version')
.required(),
});
@@ -34,21 +52,31 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
typeof validationSchema
>;
type DatabaseServiceField = Required<
Yup.InferType<typeof validationSchema>['majorVersion']
>;
export default function DatabaseServiceVersionSettings() {
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const { openDialog, closeDialog } = useDialog();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetPostgresSettingsDocument],
refetchQueries: [
GetPostgresSettingsDocument,
GetWorkspaceAndProjectDocument,
],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
...(!isPlatform ? { client: localMimirClient } : {}),
});
const {
version: postgresVersion,
postgresMajor: currentPostgresMajor,
postgresMinor: currentPostgresMinor,
error: postgresSettingsError,
loading: loadingPostgresSettings,
} = useGetPostgresVersion();
const { data: databaseVersionsData } = useGetSoftwareVersionsQuery({
variables: {
@@ -57,14 +85,11 @@ export default function DatabaseServiceVersionSettings() {
skip: !isPlatform,
});
const { version } = data?.config?.postgres || {};
const databaseVersions = databaseVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(databaseVersions.map((el) => el.version)).add(version),
new Set(databaseVersions.map((el) => el.version)).add(postgresVersion),
)
.sort()
.reverse()
.map((availableVersion) => ({
label: availableVersion,
value: availableVersion,
@@ -72,46 +97,140 @@ export default function DatabaseServiceVersionSettings() {
const form = useForm<DatabaseServiceVersionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { version: { label: '', value: '' } },
defaultValues: {
minorVersion: { label: '', value: '' },
majorVersion: { label: '', value: '' },
},
resolver: yupResolver(validationSchema),
});
const { formState, watch } = form;
const selectedMajor = watch('majorVersion').value;
const selectedMinor = watch('minorVersion').value;
const getMajorAndMinorVersions = (): {
availableMajorVersions: DatabaseServiceField[];
majorToMinorVersions: Record<string, DatabaseServiceField[]>;
} => {
const majorToMinorVersions = {};
const availableMajorVersions = [];
availableVersions.forEach((availableVersion) => {
if (!availableVersion.value) {
return;
}
const [major, minor] = availableVersion.value.split('.');
// Don't suggest versions that are lower than the current Postgres major version (can't downgrade)
if (Number(major) < Number(currentPostgresMajor)) {
return;
}
if (availableMajorVersions.every((item) => item.value !== major)) {
availableMajorVersions.push({
label: major,
value: major,
});
}
if (!majorToMinorVersions[major]) {
majorToMinorVersions[major] = [];
}
majorToMinorVersions[major].push({
label: minor,
value: minor,
});
});
return {
availableMajorVersions,
majorToMinorVersions,
};
};
const { availableMajorVersions, majorToMinorVersions } = useMemo(
getMajorAndMinorVersions,
[availableVersions, currentPostgresMajor],
);
const availableMinorVersions = majorToMinorVersions[selectedMajor] || [];
useEffect(() => {
if (!loading && version) {
if (
!loadingPostgresSettings &&
currentPostgresMajor &&
currentPostgresMinor
) {
form.reset({
version: {
label: version,
value: version,
majorVersion: {
label: currentPostgresMajor,
value: currentPostgresMajor,
},
minorVersion: {
label: currentPostgresMinor,
value: currentPostgresMinor,
},
});
}
}, [loading, version, form]);
}, [
loadingPostgresSettings,
currentPostgresMajor,
currentPostgresMinor,
form,
]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Postgres version..."
className="justify-center"
/>
);
}
const { isMigrating, shouldShowUpgradeLogs } = useIsDatabaseMigrating({
shouldPoll: true,
});
if (error) {
throw error;
}
const showMigrateWarning =
Number(selectedMajor) > Number(currentPostgresMajor);
const { formState } = form;
const { state } = useAppState();
const applicationUpdating =
state === ApplicationStatus.Updating ||
state === ApplicationStatus.Migrating;
const applicationUnhealthy =
state !== ApplicationStatus.Live && !applicationUpdating;
const isMajorVersionDirty = formState?.dirtyFields?.majorVersion;
const isMinorVersionDirty = formState?.dirtyFields?.minorVersion;
const isDirty = isMajorVersionDirty || isMinorVersionDirty;
const versionFieldsDisabled =
applicationUpdating || applicationUnhealthy || maintenanceActive;
const saveDisabled = versionFieldsDisabled || !isDirty;
const handleDatabaseServiceVersionsChange = async (
formValues: DatabaseServiceVersionFormValues,
) => {
const newVersion = `${formValues.majorVersion.value}.${formValues.minorVersion.value}`;
// Major version change
if (isMajorVersionDirty) {
openDialog({
title: 'Update Postgres MAJOR version',
component: (
<DatabaseMigrateVersionConfirmationDialog
postgresVersion={newVersion}
onCancel={() => {}}
onProceed={() => {}}
/>
),
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
return;
}
// Minor version change
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
postgres: {
version: formValues.version.value,
version: newVersion,
},
},
},
@@ -143,6 +262,33 @@ export default function DatabaseServiceVersionSettings() {
);
};
const openLatestUpgradeLogsModal = async () => {
openDialog({
component: <DatabaseMigrateLogsModal />,
props: {
PaperProps: { className: 'p-0 max-w-2xl w-full' },
titleProps: {
onClose: closeDialog,
},
},
title: 'Postgres upgrade logs',
});
};
if (loadingPostgresSettings) {
return (
<ActivityIndicator
delay={1000}
label="Loading Postgres version..."
className="justify-center"
/>
);
}
if (postgresSettingsError) {
throw postgresSettingsError;
}
return (
<FormProvider {...form}>
<Form onSubmit={handleDatabaseServiceVersionsChange}>
@@ -151,54 +297,144 @@ export default function DatabaseServiceVersionSettings() {
description="The version of Postgres to use."
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
disabled: saveDisabled,
loading: formState.isSubmitting,
},
}}
docsLink="https://hub.docker.com/r/nhost/postgres/tags"
docsTitle="the latest releases"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
className="flex flex-col"
topRightElement={
shouldShowUpgradeLogs ? (
<Button
variant="outlined"
color="primary"
size="medium"
className="self-center"
onClick={openLatestUpgradeLogsModal}
startIcon={<RepeatIcon className="h-4 w-4" />}
>
View latest upgrade logs
</Button>
) : null
}
>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
<Box className="grid grid-flow-row gap-x-4 gap-y-2 lg:grid-cols-5">
<ControlledAutocomplete
id="majorVersion"
name="majorVersion"
autoHighlight
freeSolo
disabled={versionFieldsDisabled}
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
});
const result = [...matched, ...otherOptions];
return option.value;
}}
showCustomOption="auto"
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
return result;
}}
fullWidth
className="lg:col-span-2"
options={availableVersions}
error={!!formState.errors?.version?.message}
helperText={formState.errors?.version?.message}
showCustomOption="auto"
customOptionLabel={(value) => `Use custom value: "${value}"`}
/>
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
onChange={(_event, value) => {
if (typeof value !== 'string' && !Array.isArray(value)) {
if (value.value !== selectedMajor) {
const nextAvailableMinorVersions =
majorToMinorVersions[value.value] || [];
const isSelectedMinorAvailable =
nextAvailableMinorVersions.some(
(minor) => minor.value === selectedMinor,
);
// If the selected minor version is not available in the new major version, select the first available minor version
if (
!isSelectedMinorAvailable &&
nextAvailableMinorVersions.length > 0
) {
form.setValue(
'minorVersion',
nextAvailableMinorVersions[0],
);
}
}
form.setValue('majorVersion', value);
}
}}
fullWidth
className="lg:col-span-1"
label="MAJOR"
options={availableMajorVersions}
error={!!formState.errors?.majorVersion?.value?.message}
helperText={formState.errors?.majorVersion?.value?.message}
customOptionLabel={(value) => `Use custom value: "${value}"`}
/>
<ControlledAutocomplete
id="minorVersion"
name="minorVersion"
autoHighlight
freeSolo
disabled={versionFieldsDisabled}
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
fullWidth
className="lg:col-span-2"
label="MINOR"
options={availableMinorVersions}
error={!!formState.errors?.minorVersion?.value?.message}
helperText={formState.errors?.minorVersion?.value?.message}
showCustomOption="auto"
customOptionLabel={(value) => `Use custom value: "${value}"`}
/>
</Box>
{showMigrateWarning && <DatabaseMigrateDowntimeWarning />}
{applicationUpdating && <DatabaseUpdateInProgressWarning />}
{applicationUnhealthy && !isMigrating && (
<DatabaseMigrateDisabledError />
)}
</SettingsContainer>
</Form>
</FormProvider>

View File

@@ -0,0 +1,14 @@
import { Alert } from '@/components/ui/v2/Alert';
import { ClockIcon } from '@/components/ui/v2/icons/ClockIcon';
import { Text } from '@/components/ui/v2/Text';
export default function DatabaseMigrateWarning() {
return (
<Alert severity="warning" className="flex flex-col gap-3 text-left">
<Text className="flex items-center gap-1 font-semibold">
<ClockIcon className="h-4 w-4" /> An update is in progress
</Text>
<Text>You can edit the version only after the update is complete.</Text>
</Alert>
);
}

View File

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

View File

@@ -0,0 +1,40 @@
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import Link from 'next/link';
interface ApplicationLockedReasonProps {
reason?: string;
}
export default function ApplicationLockedReason({
reason,
}: ApplicationLockedReasonProps) {
return (
<Alert severity="warning" className="mx-auto max-w-xs gap-2 p-6 ">
<Text className="pb-4 text-left">
Your project has been temporarily locked due to the following reason:
</Text>
<Box
className="rounded-md p-2"
sx={{
backgroundColor: 'beige.main',
}}
>
<Text className="px-2 py-1 font-semibold">{reason}</Text>
</Box>
<Text className="pt-4 text-left">
Please{' '}
<Link
className="font-semibold underline underline-offset-2"
href="/support"
target="_blank"
rel="noopener noreferrer"
>
contact our support
</Link>{' '}
team for assistance.
</Text>
</Alert>
);
}

View File

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

View File

@@ -2,25 +2,24 @@ import { useDialog } from '@/components/common/DialogProvider';
import { Container } from '@/components/layout/Container';
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { ApplicationInfo } from '@/features/projects/common/components/ApplicationInfo';
import { ApplicationLockedReason } from '@/features/projects/common/components/ApplicationLockedReason';
import { ApplicationPausedReason } from '@/features/projects/common/components/ApplicationPausedReason';
import { ApplicationPausedSymbol } from '@/features/projects/common/components/ApplicationPausedSymbol';
import { ChangePlanModal } from '@/features/projects/common/components/ChangePlanModal';
import { RemoveApplicationModal } from '@/features/projects/common/components/RemoveApplicationModal';
import { StagingMetadata } from '@/features/projects/common/components/StagingMetadata';
import { useAppPausedReason } from '@/features/projects/common/hooks/useAppPausedReason';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import {
GetAllWorkspacesAndProjectsDocument,
useGetFreeAndActiveProjectsQuery,
useUnpauseApplicationMutation,
} from '@/generated/graphql';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useState } from 'react';
export default function ApplicationPaused() {
@@ -28,7 +27,6 @@ export default function ApplicationPaused() {
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const isOwner = useIsCurrentUserOwner();
const user = useUserData();
const [showDeletingModal, setShowDeletingModal] = useState(false);
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
@@ -36,13 +34,8 @@ export default function ApplicationPaused() {
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
});
const { data, loading } = useGetFreeAndActiveProjectsQuery({
variables: { userId: user?.id },
skip: !user,
});
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
const { isLocked, lockedReason, freeAndLiveProjectsNumberExceeded, loading } =
useAppPausedReason();
async function handleTriggerUnpausing() {
await execPromiseWithErrorToast(
@@ -77,75 +70,67 @@ export default function ApplicationPaused() {
/>
</Modal>
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-4 text-center">
<Container className="mx-auto mt-20 grid max-w-lg grid-flow-row gap-6 text-center">
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/assets/PausedApp.svg"
alt="Closed Eye"
width={72}
height={72}
/>
<ApplicationPausedSymbol isLocked={isLocked} />
</div>
<Box className="grid grid-flow-row gap-1">
<Box className="grid grid-flow-row gap-6">
<Text variant="h3" component="h1">
{currentProject.name} is sleeping
{currentProject.name} is {isLocked ? 'locked' : 'paused'}
</Text>
{isLocked ? (
<ApplicationLockedReason reason={lockedReason} />
) : (
<>
<ApplicationPausedReason
freeAndLiveProjectsNumberExceeded={
freeAndLiveProjectsNumberExceeded
}
/>
<div className="grid grid-flow-row gap-4">
{isOwner && (
<Button
className="mx-auto w-full max-w-xs"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
maxWidth: 'lg',
},
});
}}
>
Upgrade to Pro
</Button>
)}
<Button
variant="borderless"
className="mx-auto w-full max-w-xs"
loading={changingApplicationStateLoading}
disabled={
changingApplicationStateLoading ||
freeAndLiveProjectsNumberExceeded
}
onClick={handleTriggerUnpausing}
>
Wake Up
</Button>
<Text>
Starter projects stop responding to API calls after 7 days of
inactivity. Upgrade to Pro to avoid autosleep.
</Text>
</Box>
<Box className="grid grid-flow-row gap-2">
{isOwner && (
<Button
className="mx-auto w-full max-w-[280px]"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
maxWidth: 'lg',
},
});
}}
>
Upgrade to Pro
</Button>
{isOwner && (
<Button
color="error"
variant="outlined"
className="mx-auto w-full max-w-xs"
onClick={() => setShowDeletingModal(true)}
>
Delete Project
</Button>
)}
</div>
</>
)}
<div className="grid grid-flow-row gap-2">
<Button
variant="borderless"
className="mx-auto w-full max-w-[280px]"
loading={changingApplicationStateLoading}
disabled={changingApplicationStateLoading || wakeUpDisabled}
onClick={handleTriggerUnpausing}
>
Wake Up
</Button>
{wakeUpDisabled && (
<Alert severity="warning" className="mx-auto max-w-xs text-left">
Note: Only one free project can be active at any given time.
Please pause your active free project before unpausing{' '}
{currentProject.name}.
</Alert>
)}
{isOwner && (
<Button
color="error"
variant="borderless"
className="mx-auto w-full max-w-[280px]"
onClick={() => setShowDeletingModal(true)}
>
Delete Project
</Button>
)}
</div>
</Box>
<StagingMetadata>

View File

@@ -0,0 +1,31 @@
import { Alert } from '@/components/ui/v2/Alert';
import { Text } from '@/components/ui/v2/Text';
interface ApplicationPausedReasonProps {
freeAndLiveProjectsNumberExceeded?: boolean;
}
export default function ApplicationPausedReason({
freeAndLiveProjectsNumberExceeded,
}: ApplicationPausedReasonProps) {
return (
<Alert
severity="warning"
className="mx-auto flex max-w-xs flex-col gap-4 p-6 text-left"
>
<Text>
Starter projects will stop responding to API calls after 7 days of
inactivity, so consider
<span className="font-semibold"> upgrading to Pro </span>to avoid
auto-sleep.
</Text>
{freeAndLiveProjectsNumberExceeded && (
<Text>
Additionally, only 1 free project can be active at any given time, so
please pause your current active free project before unpausing
another.
</Text>
)}
</Alert>
);
}

View File

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

View File

@@ -0,0 +1,23 @@
import Image from 'next/image';
export default function ApplicationPausedSymbol({
isLocked,
}: {
isLocked?: boolean;
}) {
if (isLocked) {
return (
<Image src="/assets/LockedApp.svg" alt="Lock" width={72} height={72} />
);
}
// paused
return (
<Image
src="/assets/PausedApp.svg"
alt="Closed Eye"
width={72}
height={72}
/>
);
}

View File

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

View File

@@ -2,8 +2,8 @@ import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { BaseDialog } from '@/components/ui/v2/Dialog';
import { Radio } from '@/components/ui/v2/Radio';
import { Text } from '@/components/ui/v2/Text';
import { useAppState } from '@/features/projects/common/hooks/useAppState';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
@@ -31,7 +31,7 @@ function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
>
<div className="grid grid-flow-row gap-y-0.5">
<div className="grid grid-flow-col items-center justify-start gap-2">
<Checkbox
<Radio
onChange={setPlan}
checked={selectedPlanId === planId}
aria-label={planName}
@@ -241,7 +241,21 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
))}
</div>
<div className="mt-2 grid grid-flow-row gap-2">
<div className="mt-0">
<Text variant="subtitle2" className="w-full px-1">
For a complete list of features, visit our{' '}
<a
href="https://nhost.io/pricing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
pricing page
</a>
</Text>
</div>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
onClick={handleChangePlanClick}
disabled={!selectedPlan}

View File

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

View File

@@ -0,0 +1,45 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
import {
useGetFreeAndActiveProjectsQuery,
useGetProjectIsLockedQuery,
} from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
/**
* This hook returns the reason why the application is paused.
* It returns the locked reason and if the user has exceeded the number of free and live projects.
*/
export default function useAppPausedReason(): {
isLocked: boolean;
lockedReason: string | undefined;
freeAndLiveProjectsNumberExceeded: boolean;
loading: boolean;
} {
const { currentProject } = useCurrentWorkspaceAndProject();
const user = useUserData();
const { data, loading } = useGetFreeAndActiveProjectsQuery({
variables: { userId: user?.id },
skip: !user,
});
const { data: isLockedData } = useGetProjectIsLockedQuery({
variables: { appId: currentProject.id },
skip: !currentProject,
});
const isLocked = isLockedData?.app?.isLocked;
const lockedReason = isLockedData?.app?.isLockedReason;
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
const freeAndLiveProjectsNumberExceeded =
numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
return {
isLocked,
lockedReason,
freeAndLiveProjectsNumberExceeded,
loading,
};
}

View File

@@ -38,6 +38,13 @@ export default function useNavigationVisible() {
return false;
}
if (
state === ApplicationStatus.Migrating &&
currentProject.desiredState === ApplicationStatus.Live
) {
return true;
}
if (
state === ApplicationStatus.Live ||
state === ApplicationStatus.Updating

View File

@@ -1,6 +1,6 @@
const planDescriptions = {
Starter: '1 GB database, 5 GB of file storage, 10 GB of network traffic.',
Pro: '10 GB database, 25 GB of file storage, 50 GB of network traffic, and backups.',
Pro: '10 GB database, 50 GB of file storage, 50 GB of network traffic, and backups.',
Team: 'Reach out to us at support@nhost.io to have your private channel set up.',
};

View File

@@ -0,0 +1,305 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateRateLimitConfigMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import { useGetRateLimits } from 'features/projects/rate-limiting/settings/hooks/useGetRateLimits';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
bruteForce: rateLimitingItemValidationSchema,
emails: rateLimitingItemValidationSchema,
global: rateLimitingItemValidationSchema,
signups: rateLimitingItemValidationSchema,
sms: rateLimitingItemValidationSchema,
});
export type AuthLimitingFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthLimitingForm() {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRateLimitConfig] = useUpdateRateLimitConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { authRateLimit, loading } = useGetRateLimits();
const {
bruteForce,
emails,
global,
signups,
sms,
enabled: authRateEnabled,
} = authRateLimit;
const {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
} = bruteForce;
const {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
} = emails;
const {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
} = global;
const {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
} = signups;
const {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
} = sms;
const form = useForm<AuthLimitingFormValues>({
defaultValues: {
enabled: authRateEnabled,
bruteForce: {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
},
emails: {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
},
global: {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
},
signups: {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
},
sms: {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
},
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && authRateEnabled) {
form.reset({
enabled: authRateEnabled,
bruteForce: {
limit: bruteForceLimit,
interval: bruteForceInterval,
intervalUnit: bruteForceIntervalUnit,
},
emails: {
limit: emailsLimit,
interval: emailsInterval,
intervalUnit: emailsIntervalUnit,
},
global: {
limit: globalLimit,
interval: globalInterval,
intervalUnit: globalIntervalUnit,
},
signups: {
limit: signupsLimit,
interval: signupsInterval,
intervalUnit: signupsIntervalUnit,
},
sms: {
limit: smsLimit,
interval: smsInterval,
intervalUnit: smsIntervalUnit,
},
});
}
}, [
loading,
form,
authRateEnabled,
bruteForceLimit,
bruteForceInterval,
bruteForceIntervalUnit,
emailsLimit,
emailsInterval,
emailsIntervalUnit,
globalLimit,
globalInterval,
globalIntervalUnit,
signupsLimit,
signupsInterval,
signupsIntervalUnit,
smsLimit,
smsInterval,
smsIntervalUnit,
]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: AuthLimitingFormValues) => {
const updateConfigPromise = updateRateLimitConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
rateLimit: formValues.enabled
? {
bruteForce: {
limit: formValues.bruteForce.limit,
interval: `${formValues.bruteForce.interval}${formValues.bruteForce.intervalUnit}`,
},
emails: {
limit: formValues.emails.limit,
interval: `${formValues.emails.interval}${formValues.emails.intervalUnit}`,
},
global: {
limit: formValues.global.limit,
interval: `${formValues.global.interval}${formValues.global.intervalUnit}`,
},
signups: {
limit: formValues.signups.limit,
interval: `${formValues.signups.interval}${formValues.signups.intervalUnit}`,
},
sms: {
limit: formValues.sms.limit,
interval: `${formValues.sms.interval}${formValues.sms.intervalUnit}`,
},
}
: null,
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating Auth rate limit settings...',
successMessage: 'Auth rate limit settings updated successfully',
errorMessage: 'Failed to update Auth rate limit settings',
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title="Auth"
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.bruteForce}
id="bruteForce"
title="Brute Force"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.emails}
id="emails"
title="Emails"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.global}
id="global"
title="Global"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.signups}
id="signups"
title="Signups"
/>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.sms}
id="sms"
title="SMS"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,88 @@
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import { intervalUnitOptions } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import type {
FieldError,
FieldErrorsImpl,
Merge,
UseFormRegister,
} from 'react-hook-form';
interface RateLimitFieldProps {
register: UseFormRegister<any>;
errors: Merge<
FieldError,
FieldErrorsImpl<{
limit: number;
interval: number;
intervalUnit: string;
}>
>;
disabled?: boolean;
title?: string;
id: string;
}
export default function RateLimitField({
register,
disabled,
id,
errors,
title,
}: RateLimitFieldProps) {
return (
<Box className="px-4">
{title ? <Text className="py-4 font-semibold">{title}</Text> : null}
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex flex-row items-center gap-2">
<Text>Limit</Text>
<Input
{...register(`${id}.limit`)}
disabled={disabled}
id={`${id}.limit`}
type="number"
placeholder=""
className="max-w-60"
hideEmptyHelperText
error={!!errors?.limit}
helperText={errors?.limit?.message}
autoComplete="off"
/>
</div>
<div className="flex flex-row items-center gap-2">
<Text>Interval</Text>
<Input
{...register(`${id}.interval`)}
disabled={disabled}
id={`${id}.interval`}
type="number"
placeholder=""
hideEmptyHelperText
className="max-w-32"
error={!!errors?.interval}
helperText={errors?.interval?.message}
autoComplete="off"
/>
<ControlledSelect
{...register(`${id}.intervalUnit`)}
disabled={disabled}
variant="normal"
id={`${id}.intervalUnit`}
className="w-27"
defaultValue="m"
hideEmptyHelperText
>
{intervalUnitOptions.map(({ value, label }) => (
<Option key={`${id}.intervalUnit.${value}`} value={value}>
{label}
</Option>
))}
</ControlledSelect>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,169 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
useUpdateRateLimitConfigMutation,
type ConfigConfigUpdateInput,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
rateLimit: rateLimitingItemValidationSchema,
});
export interface RateLimitDefaultValues {
enabled: boolean;
rateLimit: { limit: number; interval: number; intervalUnit: string };
}
export interface RateLimitingFormProps {
defaultValues: RateLimitDefaultValues;
serviceName: keyof ConfigConfigUpdateInput;
title: string;
loading: boolean;
}
export type RateLimitingFormValues = Yup.InferType<typeof validationSchema>;
export default function RateLimitingForm({
defaultValues,
serviceName,
title,
loading,
}: RateLimitingFormProps) {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRateLimitConfig] = useUpdateRateLimitConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<RateLimitingFormValues>({
defaultValues: defaultValues.enabled
? defaultValues
: {
enabled: false,
rateLimit: {
limit: 0,
interval: 0,
intervalUnit: 's',
},
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && defaultValues.enabled) {
form.reset(defaultValues);
}
}, [loading, defaultValues, form]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: RateLimitingFormValues) => {
const updateConfigPromise = updateRateLimitConfig({
variables: {
appId: currentProject.id,
config: {
[serviceName]: {
rateLimit: formValues.enabled
? {
limit: formValues.rateLimit.limit,
interval: `${formValues.rateLimit.interval}${formValues.rateLimit.intervalUnit}`,
}
: null,
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: `Updating ${title} rate limit settings...`,
successMessage: `${title} rate limit settings updated successfully`,
errorMessage: `Failed to update ${title} rate limit settings`,
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title={title}
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
<RateLimitField
disabled={!enabled}
register={register}
errors={errors.rateLimit}
id="rateLimit"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,194 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Divider } from '@/components/ui/v2/Divider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { rateLimitingItemValidationSchema } from '@/features/projects/rate-limiting/settings/components/validationSchemas';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateRunServiceConfigMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { RateLimitField } from 'features/projects/rate-limiting/settings/components/RateLimitField';
import type { UseGetRunServiceRateLimitsReturn } from 'features/projects/rate-limiting/settings/hooks/useGetRunServiceRateLimits/useGetRunServiceRateLimits';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
ports: Yup.array().of(rateLimitingItemValidationSchema),
});
export type RunServiceLimitingFormValues = Yup.InferType<
typeof validationSchema
>;
export interface RunServiceLimitingFormProps {
title?: string;
serviceId?: string;
loading?: boolean;
enabledDefault?: boolean;
ports?: UseGetRunServiceRateLimitsReturn['services'][0]['ports'];
}
export default function RunServiceLimitingForm({
title,
serviceId,
ports,
loading,
enabledDefault,
}: RunServiceLimitingFormProps) {
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const localMimirClient = useLocalMimirClient();
const [updateRunServiceRateLimit] = useUpdateRunServiceConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<RunServiceLimitingFormValues>({
defaultValues: {
enabled: enabledDefault,
ports: [
...ports.map((port) => ({
limit: port?.rateLimit?.limit,
interval: port?.rateLimit?.interval,
intervalUnit: port?.rateLimit?.intervalUnit,
})),
],
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && enabledDefault) {
form.reset({
enabled: enabledDefault,
ports: [
...ports.map((port) => ({
limit: port?.rateLimit?.limit,
interval: port?.rateLimit?.interval,
intervalUnit: port?.rateLimit?.intervalUnit,
})),
],
});
}
}, [loading, enabledDefault, ports, form]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading rate limits..."
className="justify-center"
/>
);
}
const {
register,
formState: { errors },
formState,
watch,
} = form;
const enabled = watch('enabled');
const handleSubmit = async (formValues: RunServiceLimitingFormValues) => {
const updateConfigPromise = updateRunServiceRateLimit({
variables: {
appID: currentProject?.id,
serviceID: serviceId,
config: {
ports: ports.map((port, index) => {
const rateLimit = formValues.ports[index];
return {
...port,
rateLimit: enabled
? {
limit: rateLimit.limit,
interval: `${rateLimit.interval}${rateLimit.intervalUnit}`,
}
: null,
};
}),
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating Run service rate limit settings...',
successMessage: 'Run service rate limit settings updated successfully',
errorMessage: 'Failed to update Run service rate limit settings',
},
);
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden"
>
<SettingsContainer
title={title}
switchId="enabled"
showSwitch
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col px-0"
>
<Divider />
{ports.map((port, index) => {
if (port?.type !== 'http' || !port?.publish) {
return null;
}
const fieldTitle = `${port.type} <-> ${port.port}`.toUpperCase();
const showDivider = index < ports.length - 1;
return (
<div key={`ports.${port.port}`}>
<RateLimitField
title={fieldTitle}
disabled={!enabled}
register={register}
errors={errors.ports}
id={`ports.${index}`}
/>
{showDivider && <Divider />}
</div>
);
})}
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,23 @@
import * as Yup from 'yup';
export const rateLimitingItemValidationSchema = Yup.object({
limit: Yup.number()
.required('Limit is required.')
.min(1)
.positive('Limit must be a positive number')
.typeError('Limit must be a number.'),
interval: Yup.number()
.required('Interval is required.')
.min(1)
.positive('Interval must be a positive number')
.typeError('Interval must be a number.'),
intervalUnit: Yup.string()
.required('Interval unit is required.')
.oneOf(['s', 'm', 'h']),
});
export const intervalUnitOptions = [
{ value: 's', label: 'seconds' },
{ value: 'm', label: 'minutes' },
{ value: 'h', label: 'hours' },
];

View File

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

View File

@@ -0,0 +1,120 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { useGetRateLimitConfigQuery } from '@/utils/__generated__/graphql';
import { DEFAULT_RATE_LIMITS } from 'features/projects/rate-limiting/settings/utils/constants';
import { parseIntervalNameUnit } from 'features/projects/rate-limiting/settings/utils/parseIntervalNameUnit';
export default function useGetRateLimits() {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { data, loading } = useGetRateLimitConfigQuery({
variables: {
appId: currentProject?.id,
resolve: false,
},
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),
});
const authRateLimit = data?.config?.auth?.rateLimit;
const hasuraRateLimit = data?.config?.hasura?.rateLimit;
const storageRateLimit = data?.config?.storage?.rateLimit;
const functionsRateLimit = data?.config?.functions?.rateLimit;
const { bruteForce, emails, global, signups, sms } = authRateLimit || {};
const { limit: bruteForceLimit, interval: bruteForceIntervalStr } =
bruteForce || {};
const { interval: bruteForceInterval, intervalUnit: bruteForceIntervalUnit } =
parseIntervalNameUnit(bruteForceIntervalStr);
const { limit: emailsLimit, interval: emailsIntervalStr } = emails || {};
const { interval: emailsInterval, intervalUnit: emailsIntervalUnit } =
parseIntervalNameUnit(emailsIntervalStr);
const { limit: globalLimit, interval: globalIntervalStr } = global || {};
const { interval: globalInterval, intervalUnit: globalIntervalUnit } =
parseIntervalNameUnit(globalIntervalStr);
const { limit: signupsLimit, interval: signupsIntervalStr } = signups || {};
const { interval: signupsInterval, intervalUnit: signupsIntervalUnit } =
parseIntervalNameUnit(signupsIntervalStr);
const { limit: smsLimit, interval: smsIntervalStr } = sms || {};
const { interval: smsInterval, intervalUnit: smsIntervalUnit } =
parseIntervalNameUnit(smsIntervalStr);
const { limit: hasuraLimit, interval: hasuraIntervalStr } =
hasuraRateLimit || {};
const { interval: hasuraInterval, intervalUnit: hasuraIntervalUnit } =
parseIntervalNameUnit(hasuraIntervalStr);
const { limit: storageLimit, interval: storageIntervalStr } =
storageRateLimit || {};
const { interval: storageInterval, intervalUnit: storageIntervalUnit } =
parseIntervalNameUnit(storageIntervalStr);
const { limit: functionsLimit, interval: functionsIntervalStr } =
functionsRateLimit || {};
const { interval: functionsInterval, intervalUnit: functionsIntervalUnit } =
parseIntervalNameUnit(functionsIntervalStr);
return {
authRateLimit: {
enabled: !!authRateLimit,
bruteForce: {
limit: bruteForceLimit || DEFAULT_RATE_LIMITS.limit,
interval: bruteForceInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit:
bruteForceIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
emails: {
limit: emailsLimit || DEFAULT_RATE_LIMITS.limit,
interval: emailsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: emailsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
global: {
limit: globalLimit || DEFAULT_RATE_LIMITS.limit,
interval: globalInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: globalIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
signups: {
limit: signupsLimit || DEFAULT_RATE_LIMITS.limit,
interval: signupsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: signupsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
sms: {
limit: smsLimit || DEFAULT_RATE_LIMITS.limit,
interval: smsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: smsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
hasuraDefaultValues: {
enabled: !!hasuraRateLimit,
rateLimit: {
limit: hasuraLimit || DEFAULT_RATE_LIMITS.limit,
interval: hasuraInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: hasuraIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
storageDefaultValues: {
enabled: !!storageRateLimit,
rateLimit: {
limit: storageLimit || DEFAULT_RATE_LIMITS.limit,
interval: storageInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: storageIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
functionsDefaultValues: {
enabled: !!functionsRateLimit,
rateLimit: {
limit: functionsLimit || DEFAULT_RATE_LIMITS.limit,
interval: functionsInterval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: functionsIntervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
},
},
loading,
};
}

View File

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

View File

@@ -0,0 +1,107 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import {
useGetLocalRunServiceRateLimitQuery,
useGetRunServicesRateLimitQuery,
type GetRunServicesRateLimitQuery,
} from '@/utils/__generated__/graphql';
import { DEFAULT_RATE_LIMITS } from 'features/projects/rate-limiting/settings/utils/constants';
import { parseIntervalNameUnit } from 'features/projects/rate-limiting/settings/utils/parseIntervalNameUnit';
import { useMemo } from 'react';
type RunService = Pick<
GetRunServicesRateLimitQuery['app']['runServices'][0],
'config'
> & {
id?: string;
serviceID?: string;
createdAt?: string;
updatedAt?: string;
subdomain?: string;
};
export interface UseGetRunServiceRateLimitsReturn {
services: {
name?: string;
id?: string;
enabled?: boolean;
ports?: {
type?: string;
port?: string;
publish?: boolean;
rateLimit?: {
limit?: number;
interval?: number;
intervalUnit?: string;
};
}[];
}[];
loading: boolean;
}
export default function useGetRunServiceRateLimits(): UseGetRunServiceRateLimitsReturn {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { data, loading: loadingPlatformServices } =
useGetRunServicesRateLimitQuery({
variables: {
appID: currentProject?.id,
resolve: false,
},
skip: !isPlatform,
});
const { loading: loadingLocalServices, data: localServicesData } =
useGetLocalRunServiceRateLimitQuery({
variables: { appID: currentProject?.id, resolve: false },
skip: isPlatform,
client: localMimirClient,
});
const platformServices = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const localServices = useMemo(
() => localServicesData?.runServiceConfigs.map((service) => service) ?? [],
[localServicesData],
);
const services: RunService[] = isPlatform ? platformServices : localServices;
const loading = isPlatform ? loadingPlatformServices : loadingLocalServices;
const servicesInfo = services.map((service) => {
const enabled = service?.config?.ports?.some(
(port) => port?.rateLimit && port?.type === 'http' && port?.publish,
);
const ports = service?.config?.ports?.map((port) => {
const { interval, intervalUnit } = parseIntervalNameUnit(
port?.rateLimit?.interval,
);
const rateLimit = {
limit: port?.rateLimit?.limit || DEFAULT_RATE_LIMITS.limit,
interval: interval || DEFAULT_RATE_LIMITS.interval,
intervalUnit: intervalUnit || DEFAULT_RATE_LIMITS.intervalUnit,
};
return {
type: port?.type,
publish: port?.publish,
port: port?.port,
rateLimit,
};
});
return {
enabled,
name: service.config?.name,
id: service.id ?? service.serviceID,
ports,
};
});
return { services: servicesInfo, loading };
}

View File

@@ -0,0 +1,5 @@
export const DEFAULT_RATE_LIMITS = {
limit: 1000,
interval: 5,
intervalUnit: 'm',
};

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
export default function parseIntervalNameUnit(interval: string) {
if (!interval) {
return {};
}
const regex = /^(\d+)([a-zA-Z])$/;
const match = interval.match(regex);
if (!match) {
return {};
}
const [, intervalValue, intervalUnit] = match;
return {
interval: parseInt(intervalValue, 10),
intervalUnit,
};
}

View File

@@ -31,6 +31,7 @@ import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { removeTypename } from '@/utils/helpers';
import {
useInsertRunServiceConfigMutation,
useInsertRunServiceMutation,
@@ -99,38 +100,50 @@ export default function ServiceForm({
}, [isDirty, location, onDirtyStateChange]);
const getFormattedConfig = (values: ServiceFormValues) => {
// Remove any __typename property from the values
const sanitizedValues = removeTypename(values) as ServiceFormValues;
const sanitizedInitialDataPorts = initialData?.ports
? removeTypename(initialData.ports)
: [];
const config: ConfigRunServiceConfigInsertInput = {
name: values.name,
name: sanitizedValues.name,
image: {
image: values.image,
image: sanitizedValues.image,
},
command: parse(values.command).map((item) => item.toString()),
command: parse(sanitizedValues.command).map((item) => item.toString()),
resources: {
compute: {
cpu: values.compute.cpu,
memory: values.compute.memory,
cpu: sanitizedValues.compute.cpu,
memory: sanitizedValues.compute.memory,
},
storage: values.storage.map((item) => ({
storage: sanitizedValues.storage.map((item) => ({
name: item.name,
path: item.path,
capacity: item.capacity,
})),
replicas: values.replicas,
replicas: sanitizedValues.replicas,
},
environment: values.environment.map((item) => ({
environment: sanitizedValues.environment.map((item) => ({
name: item.name,
value: item.value,
})),
ports: values.ports.map((item) => ({
ports: sanitizedValues.ports.map((item) => ({
port: item.port,
type: item.type,
publish: item.publish,
ingresses: item.ingresses,
rateLimit:
sanitizedInitialDataPorts.find(
(port) => port.port === item.port && port.type === item.type,
)?.rateLimit ?? null,
})),
healthCheck: values.healthCheck
healthCheck: sanitizedValues.healthCheck
? {
port: values.healthCheck?.port,
initialDelaySeconds: values.healthCheck?.initialDelaySeconds,
probePeriodSeconds: values.healthCheck?.probePeriodSeconds,
port: sanitizedValues.healthCheck?.port,
initialDelaySeconds:
sanitizedValues.healthCheck?.initialDelaySeconds,
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
}
: null,
};
@@ -303,7 +316,7 @@ export default function ServiceForm({
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -343,7 +356,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -374,7 +387,7 @@ export default function ServiceForm({
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -422,7 +435,7 @@ export default function ServiceForm({
{createServiceFormError && (
<Alert
severity="error"
className="grid items-center justify-between grid-flow-col px-4 py-3"
className="grid grid-flow-col items-center justify-between px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}

View File

@@ -30,6 +30,13 @@ export const validationSchema = Yup.object({
port: Yup.number().required(),
type: Yup.mixed<PortTypes>().oneOf(Object.values(PortTypes)).required(),
publish: Yup.boolean().default(false),
ingresses: Yup.array()
.of(
Yup.object().shape({
fqdn: Yup.array().of(Yup.string()),
}),
)
.nullable(),
}),
),
storage: Yup.array().of(
@@ -62,7 +69,16 @@ export interface ServiceFormProps extends DialogFormProps {
/**
* if there is initialData then it's an update operation
*/
initialData?: ServiceFormValues & { subdomain?: string }; // subdomain is only set on the backend
initialData?: Omit<ServiceFormValues, 'ports'> & {
subdomain?: string;
ports: {
port: number;
type: PortTypes;
publish: boolean;
ingresses?: { fqdn?: string[] }[] | null;
rateLimit?: { limit: number; interval: string } | null;
}[];
}; // subdomain is only set on the backend
/**
* Function to be called when the operation is cancelled.

View File

@@ -13,6 +13,7 @@ import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import { PortTypes } from '@/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import { type ServiceFormValues } from '@/features/services/components/ServiceForm/ServiceFormTypes';
import { getRunServicePortURL } from '@/utils/helpers';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
export default function PortsFormSection() {
@@ -40,14 +41,8 @@ export default function PortsFormSection() {
formValues.ports[index]?.type === PortTypes.HTTP &&
formValues.ports[index]?.publish;
const getPortURL = (_port: string | number, subdomain: string) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
};
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="p-4 space-y-4 rounded border-1">
<Box className="flex flex-row items-center justify-between ">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
@@ -69,14 +64,14 @@ export default function PortsFormSection() {
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() => append({ port: null, type: null, publish: false })}
>
<PlusIcon className="h-5 w-5" />
<PlusIcon className="w-5 h-5" />
</Button>
</Box>
@@ -133,16 +128,18 @@ export default function PortsFormSection() {
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
<TrashIcon className="w-4 h-4" />
</Button>
</Box>
{showURL(index) && (
<InfoCard
title="URL"
value={getPortURL(
formValues.ports[index]?.port,
formValues.subdomain,
value={getRunServicePortURL(
formValues?.subdomain,
currentProject?.region.name,
currentProject?.region.domain,
formValues.ports[index],
)}
/>
)}

View File

@@ -7,6 +7,7 @@ import { Tooltip } from '@/components/ui/v2/Tooltip';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm/ServiceFormTypes';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { useState } from 'react';
export interface ServiceConfirmationDialogProps {
/**
@@ -28,10 +29,21 @@ export default function ServiceConfirmationDialog({
onCancel,
onSubmit,
}: ServiceConfirmationDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const approximatePriceForService = parseFloat(
(formValues.compute.cpu * formValues.replicas * COST_PER_VCPU).toFixed(2),
);
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await onSubmit();
} finally {
setIsSubmitting(false);
}
};
return (
<div className="grid grid-flow-row gap-6 px-6 pb-6">
<Box className="grid grid-flow-row gap-4">
@@ -74,7 +86,12 @@ export default function ServiceConfirmationDialog({
</Box>
<Box className="grid grid-flow-row gap-2">
<Button color="primary" onClick={onSubmit} autoFocus>
<Button
loading={isSubmitting}
color="primary"
onClick={handleSubmit}
autoFocus
>
Confirm
</Button>

View File

@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import { getRunServicePortURL } from '@/utils/helpers';
import type { ConfigRunServicePort } from '@/utils/__generated__/graphql';
export interface ServiceDetailsDialogProps {
@@ -32,11 +33,7 @@ export default function ServiceDetailsDialog({
const { closeDialog } = useDialog();
const getPortURL = (_port: string | number) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.name}.${currentProject?.region.domain}`;
};
const publishedPorts = ports.filter((port) => port.publish);
return (
<div className="flex flex-col gap-4 px-6 pb-6">
@@ -48,18 +45,21 @@ export default function ServiceDetailsDialog({
/>
</div>
{ports?.length > 0 && (
{publishedPorts?.length > 0 && (
<div className="flex flex-col gap-2">
<Text color="secondary">Ports</Text>
{ports
.filter((port) => port.publish)
.map((port) => (
<InfoCard
key={String(port.port)}
title={`${port.type} <--> ${port.port}`}
value={getPortURL(port.port)}
/>
))}
{publishedPorts.map((port) => (
<InfoCard
key={String(port.port)}
title={`${port.type} <--> ${port.port}`}
value={getRunServicePortURL(
subdomain,
currentProject?.region.name,
currentProject?.region.domain,
port,
)}
/>
))}
</div>
)}

View File

@@ -50,7 +50,7 @@ export default function ServicesList({
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="w-5 h-5" />
<CubeIcon className="h-5 w-5" />
<Text>Edit {service.config?.name ?? 'unset'}</Text>
</Box>
),
@@ -66,6 +66,8 @@ export default function ServicesList({
port: item.port,
type: item.type as PortTypes,
publish: item.publish,
ingresses: item.ingresses,
rateLimit: item.rateLimit,
})),
compute: service.config?.resources?.compute ?? {
cpu: 62,
@@ -106,13 +108,13 @@ export default function ServicesList({
onClick={() => viewService(service)}
>
<Box
className="flex flex-row justify-between w-full"
className="flex w-full flex-row justify-between"
sx={{
backgroundColor: 'transparent',
}}
>
<div className="flex flex-row items-center flex-1 space-x-4">
<CubeIcon className="w-5 h-5" />
<div className="flex flex-1 flex-row items-center space-x-4">
<CubeIcon className="h-5 w-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{service.config?.name ?? 'unset'}
@@ -128,7 +130,7 @@ export default function ServicesList({
</div>
</div>
<div className="flex-row items-center hidden space-x-2 md:flex">
<div className="hidden flex-row items-center space-x-2 md:flex">
<Text variant="subtitle1" className="font-mono text-xs">
{service.id ?? service.serviceID}
</Text>
@@ -141,7 +143,7 @@ export default function ServicesList({
}}
aria-label="Service Id"
>
<CopyIcon className="w-4 h-4" />
<CopyIcon className="h-4 w-4" />
</IconButton>
</div>
</Box>
@@ -171,7 +173,7 @@ export default function ServicesList({
onClick={() => viewService(service)}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="w-4 h-4" />
<UserIcon className="h-4 w-4" />
<Text className="font-medium">View Service</Text>
</Dropdown.Item>
<Divider component="li" />
@@ -181,7 +183,7 @@ export default function ServicesList({
onClick={() => deleteService(service)}
disabled={!isPlatform}
>
<TrashIcon className="w-4 h-4" />
<TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error">
Delete Service
</Text>

View File

@@ -0,0 +1,6 @@
query getProjectIsLocked($appId: uuid!) {
app(id: $appId) {
isLocked
isLockedReason
}
}

View File

@@ -0,0 +1,46 @@
query getRateLimitConfig($appId: uuid!, $resolve: Boolean!) {
config(appID: $appId, resolve: $resolve) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
mutation UpdateRateLimitConfig(
$appId: uuid!
$config: ConfigConfigUpdateInput!
) {
updateConfig(appID: $appId, config: $config) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
mutation UpdateDatabaseVersion($appId: uuid!, $version: String!) {
changeDatabaseVersion(appID: $appId, version: $version)
}

View File

@@ -0,0 +1,11 @@
query getSystemLogs(
$appID: String!
$action: String!
$from: Timestamp
$to: Timestamp
) {
systemLogs(appID: $appID, action: $action, from: $from) {
timestamp
log
}
}

View File

@@ -27,6 +27,10 @@ fragment RunServiceConfig on ConfigRunServiceConfig {
ingresses {
fqdn
}
rateLimit {
limit
interval
}
}
healthCheck {
port

View File

@@ -0,0 +1,38 @@
fragment RunServiceRateLimit on ConfigRunServiceConfig {
name
ports {
port
type
publish
rateLimit {
limit
interval
}
ingresses {
fqdn
}
}
}
query getRunServicesRateLimit($appID: uuid!, $resolve: Boolean!) {
app(id: $appID) {
runServices {
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
...RunServiceRateLimit
}
}
}
}
query getLocalRunServiceRateLimit($appID: uuid!, $resolve: Boolean!) {
runServiceConfigs(appID: $appID, resolve: $resolve) {
serviceID
config {
...RunServiceRateLimit
}
}
}

View File

@@ -43,6 +43,8 @@ export default function AppIndexPage() {
return <ApplicationUnpausing />;
case ApplicationStatus.Restoring:
return <ApplicationRestoring />;
case ApplicationStatus.Migrating:
return <ApplicationLive />;
default:
return <ApplicationUnknown />;
}

View File

@@ -48,7 +48,7 @@ export default function ServicesPage() {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="w-5 h-5" />
<CubeIcon className="h-5 w-5" />
<Text>Create a new run service</Text>
</Box>
),
@@ -104,7 +104,7 @@ export default function ServicesPage() {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="w-5 h-5" />
<CubeIcon className="h-5 w-5" />
<Text>Create a new service</Text>
</Box>
),
@@ -125,23 +125,23 @@ export default function ServicesPage() {
if (services.length === 0 && !loading) {
return (
<Container className="mx-auto space-y-5 overflow-x-hidden max-w-9xl">
<Container className="mx-auto max-w-9xl space-y-5 overflow-x-hidden">
<div className="flex flex-row place-content-end">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="w-4 h-4" />}
startIcon={<PlusIcon className="h-4 w-4" />}
disabled={!isPlatform}
>
Add service
</Button>
</div>
<Box className="flex flex-col items-center justify-center px-48 py-12 space-y-5 border rounded-lg shadow-sm">
<ServicesIcon className="w-10 h-10" />
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<ServicesIcon className="h-10 w-10" />
<div className="flex flex-col space-y-1">
<Text className="font-medium text-center" variant="h3">
<Text className="text-center font-medium" variant="h3">
No custom services are available
</Text>
<Text variant="subtitle1" className="text-center">
@@ -149,13 +149,13 @@ export default function ServicesPage() {
</Text>
</div>
{isPlatform ? (
<div className="flex flex-row rounded-lg place-content-between ">
<div className="flex flex-row place-content-between rounded-lg ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="w-4 h-4" />}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add service
</Button>
@@ -168,12 +168,12 @@ export default function ServicesPage() {
return (
<div className="flex flex-col">
<Box className="flex flex-row p-4 place-content-end border-b-1">
<Box className="flex flex-row place-content-end border-b-1 p-4">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="w-4 h-4" />}
startIcon={<PlusIcon className="h-4 w-4" />}
disabled={!isPlatform}
>
Add service

View File

@@ -0,0 +1,90 @@
import { Container } from '@/components/layout/Container';
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { Box } from '@/components/ui/v2/Box';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { AuthLimitingForm } from '@/features/projects/rate-limiting/settings/components/AuthLimitingForm';
import { RateLimitingForm } from '@/features/projects/rate-limiting/settings/components/RateLimitingForm';
import { RunServiceLimitingForm } from '@/features/projects/rate-limiting/settings/components/RunServiceLimitingForm';
import { useGetRateLimits } from '@/features/projects/rate-limiting/settings/hooks/useGetRateLimits';
import { useGetRunServiceRateLimits } from '@/features/projects/rate-limiting/settings/hooks/useGetRunServiceRateLimits';
import { type ReactElement } from 'react';
export default function RateLimiting() {
const { services, loading } = useGetRunServiceRateLimits();
const {
hasuraDefaultValues,
functionsDefaultValues,
storageDefaultValues,
loading: loadingBaseServices,
} = useGetRateLimits();
return (
<Container
className="grid max-w-5xl grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<Box className="flex flex-row items-center gap-4 overflow-hidden rounded-lg border-1 p-4">
<div className="flex flex-col space-y-2">
<Text className="text-lg font-semibold">Rate Limiting</Text>
<Text color="secondary">
Learn more about
<Link
href="https://docs.nhost.io/platform/rate-limits"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="ml-1 font-medium"
>
Rate Limiting
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
</div>
</Box>
<AuthLimitingForm />
<RateLimitingForm
defaultValues={hasuraDefaultValues}
loading={loadingBaseServices}
serviceName="hasura"
title="Hasura"
/>
<RateLimitingForm
defaultValues={storageDefaultValues}
loading={loadingBaseServices}
serviceName="storage"
title="Storage"
/>
<RateLimitingForm
defaultValues={functionsDefaultValues}
loading={loadingBaseServices}
serviceName="functions"
title="Functions"
/>
{services?.map((service) => {
if (
service?.ports?.some((port) => port?.type === 'http' && port?.publish)
) {
return (
<RunServiceLimitingForm
enabledDefault={service.enabled}
key={service.id}
title={service.name}
serviceId={service.id}
ports={service.ports}
loading={loading}
/>
);
}
return null;
})}
</Container>
);
}
RateLimiting.getLayout = function getLayout(page: ReactElement) {
return <SettingsLayout>{page}</SettingsLayout>;
};

View File

@@ -199,6 +199,7 @@ export type ConfigAuth = {
__typename?: 'ConfigAuth';
elevatedPrivileges?: Maybe<ConfigAuthElevatedPrivileges>;
method?: Maybe<ConfigAuthMethod>;
rateLimit?: Maybe<ConfigAuthRateLimit>;
redirections?: Maybe<ConfigAuthRedirections>;
/** Resources for the service */
resources?: Maybe<ConfigResources>;
@@ -223,6 +224,7 @@ export type ConfigAuthComparisonExp = {
_or?: InputMaybe<Array<ConfigAuthComparisonExp>>;
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesComparisonExp>;
method?: InputMaybe<ConfigAuthMethodComparisonExp>;
rateLimit?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
redirections?: InputMaybe<ConfigAuthRedirectionsComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
session?: InputMaybe<ConfigAuthSessionComparisonExp>;
@@ -255,6 +257,7 @@ export type ConfigAuthElevatedPrivilegesUpdateInput = {
export type ConfigAuthInsertInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesInsertInput>;
method?: InputMaybe<ConfigAuthMethodInsertInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitInsertInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
session?: InputMaybe<ConfigAuthSessionInsertInput>;
@@ -684,6 +687,42 @@ export type ConfigAuthMethodWebauthnUpdateInput = {
relyingParty?: InputMaybe<ConfigAuthMethodWebauthnRelyingPartyUpdateInput>;
};
export type ConfigAuthRateLimit = {
__typename?: 'ConfigAuthRateLimit';
bruteForce?: Maybe<ConfigRateLimit>;
emails?: Maybe<ConfigRateLimit>;
global?: Maybe<ConfigRateLimit>;
signups?: Maybe<ConfigRateLimit>;
sms?: Maybe<ConfigRateLimit>;
};
export type ConfigAuthRateLimitComparisonExp = {
_and?: InputMaybe<Array<ConfigAuthRateLimitComparisonExp>>;
_not?: InputMaybe<ConfigAuthRateLimitComparisonExp>;
_or?: InputMaybe<Array<ConfigAuthRateLimitComparisonExp>>;
bruteForce?: InputMaybe<ConfigRateLimitComparisonExp>;
emails?: InputMaybe<ConfigRateLimitComparisonExp>;
global?: InputMaybe<ConfigRateLimitComparisonExp>;
signups?: InputMaybe<ConfigRateLimitComparisonExp>;
sms?: InputMaybe<ConfigRateLimitComparisonExp>;
};
export type ConfigAuthRateLimitInsertInput = {
bruteForce?: InputMaybe<ConfigRateLimitInsertInput>;
emails?: InputMaybe<ConfigRateLimitInsertInput>;
global?: InputMaybe<ConfigRateLimitInsertInput>;
signups?: InputMaybe<ConfigRateLimitInsertInput>;
sms?: InputMaybe<ConfigRateLimitInsertInput>;
};
export type ConfigAuthRateLimitUpdateInput = {
bruteForce?: InputMaybe<ConfigRateLimitUpdateInput>;
emails?: InputMaybe<ConfigRateLimitUpdateInput>;
global?: InputMaybe<ConfigRateLimitUpdateInput>;
signups?: InputMaybe<ConfigRateLimitUpdateInput>;
sms?: InputMaybe<ConfigRateLimitUpdateInput>;
};
export type ConfigAuthRedirections = {
__typename?: 'ConfigAuthRedirections';
/** AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS */
@@ -834,6 +873,7 @@ export type ConfigAuthTotpUpdateInput = {
export type ConfigAuthUpdateInput = {
elevatedPrivileges?: InputMaybe<ConfigAuthElevatedPrivilegesUpdateInput>;
method?: InputMaybe<ConfigAuthMethodUpdateInput>;
rateLimit?: InputMaybe<ConfigAuthRateLimitUpdateInput>;
redirections?: InputMaybe<ConfigAuthRedirectionsUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
session?: InputMaybe<ConfigAuthSessionUpdateInput>;
@@ -1233,6 +1273,7 @@ export type ConfigFloatComparisonExp = {
export type ConfigFunctions = {
__typename?: 'ConfigFunctions';
node?: Maybe<ConfigFunctionsNode>;
rateLimit?: Maybe<ConfigRateLimit>;
resources?: Maybe<ConfigFunctionsResources>;
};
@@ -1241,11 +1282,13 @@ export type ConfigFunctionsComparisonExp = {
_not?: InputMaybe<ConfigFunctionsComparisonExp>;
_or?: InputMaybe<Array<ConfigFunctionsComparisonExp>>;
node?: InputMaybe<ConfigFunctionsNodeComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigFunctionsResourcesComparisonExp>;
};
export type ConfigFunctionsInsertInput = {
node?: InputMaybe<ConfigFunctionsNodeInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigFunctionsResourcesInsertInput>;
};
@@ -1291,6 +1334,7 @@ export type ConfigFunctionsResourcesUpdateInput = {
export type ConfigFunctionsUpdateInput = {
node?: InputMaybe<ConfigFunctionsNodeUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigFunctionsResourcesUpdateInput>;
};
@@ -1415,6 +1459,7 @@ export type ConfigHasura = {
/** JWT Secrets configuration */
jwtSecrets?: Maybe<Array<ConfigJwtSecret>>;
logs?: Maybe<ConfigHasuraLogs>;
rateLimit?: Maybe<ConfigRateLimit>;
/** Resources for the service */
resources?: Maybe<ConfigResources>;
/**
@@ -1477,6 +1522,7 @@ export type ConfigHasuraComparisonExp = {
events?: InputMaybe<ConfigHasuraEventsComparisonExp>;
jwtSecrets?: InputMaybe<ConfigJwtSecretComparisonExp>;
logs?: InputMaybe<ConfigHasuraLogsComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
settings?: InputMaybe<ConfigHasuraSettingsComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>;
@@ -1510,6 +1556,7 @@ export type ConfigHasuraInsertInput = {
events?: InputMaybe<ConfigHasuraEventsInsertInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretInsertInput>>;
logs?: InputMaybe<ConfigHasuraLogsInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
settings?: InputMaybe<ConfigHasuraSettingsInsertInput>;
version?: InputMaybe<Scalars['String']>;
@@ -1602,6 +1649,7 @@ export type ConfigHasuraUpdateInput = {
events?: InputMaybe<ConfigHasuraEventsUpdateInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretUpdateInput>>;
logs?: InputMaybe<ConfigHasuraLogsUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
settings?: InputMaybe<ConfigHasuraSettingsUpdateInput>;
version?: InputMaybe<Scalars['String']>;
@@ -2013,6 +2061,30 @@ export type ConfigProviderUpdateInput = {
smtp?: InputMaybe<ConfigSmtpUpdateInput>;
};
export type ConfigRateLimit = {
__typename?: 'ConfigRateLimit';
interval: Scalars['String'];
limit: Scalars['ConfigUint32'];
};
export type ConfigRateLimitComparisonExp = {
_and?: InputMaybe<Array<ConfigRateLimitComparisonExp>>;
_not?: InputMaybe<ConfigRateLimitComparisonExp>;
_or?: InputMaybe<Array<ConfigRateLimitComparisonExp>>;
interval?: InputMaybe<ConfigStringComparisonExp>;
limit?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigRateLimitInsertInput = {
interval: Scalars['String'];
limit: Scalars['ConfigUint32'];
};
export type ConfigRateLimitUpdateInput = {
interval?: InputMaybe<Scalars['String']>;
limit?: InputMaybe<Scalars['ConfigUint32']>;
};
/** Resource configuration for a service */
export type ConfigResources = {
__typename?: 'ConfigResources';
@@ -2155,6 +2227,7 @@ export type ConfigRunServicePort = {
ingresses?: Maybe<Array<ConfigIngress>>;
port: Scalars['ConfigPort'];
publish?: Maybe<Scalars['Boolean']>;
rateLimit?: Maybe<ConfigRateLimit>;
type: Scalars['String'];
};
@@ -2165,6 +2238,7 @@ export type ConfigRunServicePortComparisonExp = {
ingresses?: InputMaybe<ConfigIngressComparisonExp>;
port?: InputMaybe<ConfigPortComparisonExp>;
publish?: InputMaybe<ConfigBooleanComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
type?: InputMaybe<ConfigStringComparisonExp>;
};
@@ -2172,6 +2246,7 @@ export type ConfigRunServicePortInsertInput = {
ingresses?: InputMaybe<Array<ConfigIngressInsertInput>>;
port: Scalars['ConfigPort'];
publish?: InputMaybe<Scalars['Boolean']>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
type: Scalars['String'];
};
@@ -2179,6 +2254,7 @@ export type ConfigRunServicePortUpdateInput = {
ingresses?: InputMaybe<Array<ConfigIngressUpdateInput>>;
port?: InputMaybe<Scalars['ConfigPort']>;
publish?: InputMaybe<Scalars['Boolean']>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
type?: InputMaybe<Scalars['String']>;
};
@@ -2386,6 +2462,7 @@ export type ConfigStandardOauthProviderWithScopeUpdateInput = {
export type ConfigStorage = {
__typename?: 'ConfigStorage';
antivirus?: Maybe<ConfigStorageAntivirus>;
rateLimit?: Maybe<ConfigRateLimit>;
/**
* Networking (custom domains at the moment) are not allowed as we need to do further
* configurations in the CDN. We will enable it again in the future.
@@ -2427,18 +2504,21 @@ export type ConfigStorageComparisonExp = {
_not?: InputMaybe<ConfigStorageComparisonExp>;
_or?: InputMaybe<Array<ConfigStorageComparisonExp>>;
antivirus?: InputMaybe<ConfigStorageAntivirusComparisonExp>;
rateLimit?: InputMaybe<ConfigRateLimitComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigStorageInsertInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusInsertInput>;
rateLimit?: InputMaybe<ConfigRateLimitInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>;
version?: InputMaybe<Scalars['String']>;
};
export type ConfigStorageUpdateInput = {
antivirus?: InputMaybe<ConfigStorageAntivirusUpdateInput>;
rateLimit?: InputMaybe<ConfigRateLimitUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>;
version?: InputMaybe<Scalars['String']>;
};
@@ -12522,12 +12602,6 @@ export type Mutation_Root = {
};
/** mutation root */
export type Mutation_RootBackupAllApplicationsDatabaseArgs = {
expireInDays?: InputMaybe<Scalars['Int']>;
};
/** mutation root */
export type Mutation_RootBackupApplicationDatabaseArgs = {
appID: Scalars['String'];
@@ -22922,6 +22996,13 @@ export type GetConfiguredVersionsQueryVariables = Exact<{
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 GetProjectIsLockedQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type GetProjectIsLockedQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', isLocked?: boolean | null, isLockedReason?: string | null } | null };
export type GetProjectLocalesQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
@@ -23009,6 +23090,22 @@ export type GetConfigRawJsonQueryVariables = Exact<{
export type GetConfigRawJsonQuery = { __typename?: 'query_root', configRawJSON: string };
export type GetRateLimitConfigQueryVariables = Exact<{
appId: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetRateLimitConfigQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }, storage?: { __typename?: 'ConfigStorage', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, functions?: { __typename?: 'ConfigFunctions', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, auth?: { __typename?: 'ConfigAuth', rateLimit?: { __typename?: 'ConfigAuthRateLimit', bruteForce?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, emails?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, global?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, signups?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, sms?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null } | null } | null };
export type UpdateRateLimitConfigMutationVariables = Exact<{
appId: Scalars['uuid'];
config: ConfigConfigUpdateInput;
}>;
export type UpdateRateLimitConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }, storage?: { __typename?: 'ConfigStorage', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, functions?: { __typename?: 'ConfigFunctions', rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null, auth?: { __typename?: 'ConfigAuth', rateLimit?: { __typename?: 'ConfigAuthRateLimit', bruteForce?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, emails?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, global?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, signups?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, sms?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null } | null } | null } };
export type ReplaceConfigRawJsonMutationVariables = Exact<{
appID: Scalars['uuid'];
rawJSON: Scalars['String'];
@@ -23081,6 +23178,14 @@ export type UpdateConfigMutationVariables = Exact<{
export type UpdateConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigPostgresResources', enablePublicAccess?: boolean | null, storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null, ai?: { __typename?: 'ConfigAI', version?: string | null, webhookSecret: string, autoEmbeddings?: { __typename?: 'ConfigAIAutoEmbeddings', synchPeriodMinutes?: any | null } | null, openai: { __typename?: 'ConfigAIOpenai', organization?: string | null, apiKey: string }, resources: { __typename?: 'ConfigAIResources', compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any } } } | null } };
export type UpdateDatabaseVersionMutationVariables = Exact<{
appId: Scalars['uuid'];
version: Scalars['String'];
}>;
export type UpdateDatabaseVersionMutation = { __typename?: 'mutation_root', changeDatabaseVersion: boolean };
export type UnpauseApplicationMutationVariables = Exact<{
appId: Scalars['uuid'];
}>;
@@ -23203,6 +23308,16 @@ export type GetServiceLabelValuesQueryVariables = Exact<{
export type GetServiceLabelValuesQuery = { __typename?: 'query_root', getServiceLabelValues: Array<string> };
export type GetSystemLogsQueryVariables = Exact<{
appID: Scalars['String'];
action: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
}>;
export type GetSystemLogsQuery = { __typename?: 'query_root', systemLogs: Array<{ __typename?: 'Log', timestamp: any, log: string }> };
export type DeletePaymentMethodMutationVariables = Exact<{
paymentMethodId: Scalars['uuid'];
}>;
@@ -23374,7 +23489,7 @@ export type GetRunServiceQueryVariables = Exact<{
export type GetRunServiceQuery = { __typename?: 'query_root', runService?: { __typename?: 'run_service', id: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null }> | null } | null } | null };
export type RunServiceConfigFragment = { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null };
export type RunServiceConfigFragment = { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null };
export type GetRunServicesQueryVariables = Exact<{
appID: Scalars['uuid'];
@@ -23384,7 +23499,7 @@ export type GetRunServicesQueryVariables = Exact<{
}>;
export type GetRunServicesQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } | null }>, runServices_aggregate: { __typename?: 'run_service_aggregate', aggregate?: { __typename?: 'run_service_aggregate_fields', count: number } | null } } | null };
export type GetRunServicesQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } | null }>, runServices_aggregate: { __typename?: 'run_service_aggregate', aggregate?: { __typename?: 'run_service_aggregate_fields', count: number } | null } } | null };
export type GetLocalRunServiceConfigsQueryVariables = Exact<{
appID: Scalars['uuid'];
@@ -23392,7 +23507,25 @@ export type GetLocalRunServiceConfigsQueryVariables = Exact<{
}>;
export type GetLocalRunServiceConfigsQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } }> };
export type GetLocalRunServiceConfigsQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, command?: Array<string> | null, image: { __typename?: 'ConfigRunServiceImage', image: string }, resources: { __typename?: 'ConfigRunServiceResources', replicas: any, compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any }, storage?: Array<{ __typename?: 'ConfigRunServiceResourcesStorage', name: any, path: string, capacity: any }> | null }, environment?: Array<{ __typename?: 'ConfigEnvironmentVariable', name: string, value: string }> | null, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null }> | null, healthCheck?: { __typename?: 'ConfigHealthCheck', port: any, initialDelaySeconds?: number | null, probePeriodSeconds?: number | null } | null } }> };
export type RunServiceRateLimitFragment = { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null };
export type GetRunServicesRateLimitQueryVariables = Exact<{
appID: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetRunServicesRateLimitQuery = { __typename?: 'query_root', app?: { __typename?: 'apps', runServices: Array<{ __typename?: 'run_service', id: any, createdAt: any, updatedAt: any, subdomain: string, config?: { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null } | null }> } | null };
export type GetLocalRunServiceRateLimitQueryVariables = Exact<{
appID: Scalars['uuid'];
resolve: Scalars['Boolean'];
}>;
export type GetLocalRunServiceRateLimitQuery = { __typename?: 'query_root', runServiceConfigs: Array<{ __typename?: 'ConfigRunServiceConfigWithID', serviceID: any, config: { __typename?: 'ConfigRunServiceConfig', name: any, ports?: Array<{ __typename?: 'ConfigRunServicePort', port: any, type: string, publish?: boolean | null, rateLimit?: { __typename?: 'ConfigRateLimit', limit: any, interval: string } | null, ingresses?: Array<{ __typename?: 'ConfigIngress', fqdn?: Array<string> | null }> | null }> | null } }> };
export type InsertRunServiceMutationVariables = Exact<{
object: Run_Service_Insert_Input;
@@ -23855,6 +23988,10 @@ export const RunServiceConfigFragmentDoc = gql`
ingresses {
fqdn
}
rateLimit {
limit
interval
}
}
healthCheck {
port
@@ -23863,6 +24000,23 @@ export const RunServiceConfigFragmentDoc = gql`
}
}
`;
export const RunServiceRateLimitFragmentDoc = gql`
fragment RunServiceRateLimit on ConfigRunServiceConfig {
name
ports {
port
type
publish
rateLimit {
limit
interval
}
ingresses {
fqdn
}
}
}
`;
export const GetWorkspaceMembersWorkspaceMemberFragmentDoc = gql`
fragment getWorkspaceMembersWorkspaceMember on workspaceMembers {
id
@@ -24826,6 +24980,45 @@ export type GetConfiguredVersionsQueryResult = Apollo.QueryResult<GetConfiguredV
export function refetchGetConfiguredVersionsQuery(variables: GetConfiguredVersionsQueryVariables) {
return { query: GetConfiguredVersionsDocument, variables: variables }
}
export const GetProjectIsLockedDocument = gql`
query getProjectIsLocked($appId: uuid!) {
app(id: $appId) {
isLocked
isLockedReason
}
}
`;
/**
* __useGetProjectIsLockedQuery__
*
* To run a query within a React component, call `useGetProjectIsLockedQuery` and pass it any options that fit your needs.
* When your component renders, `useGetProjectIsLockedQuery` 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 } = useGetProjectIsLockedQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetProjectIsLockedQuery(baseOptions: Apollo.QueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
}
export function useGetProjectIsLockedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>(GetProjectIsLockedDocument, options);
}
export type GetProjectIsLockedQueryHookResult = ReturnType<typeof useGetProjectIsLockedQuery>;
export type GetProjectIsLockedLazyQueryHookResult = ReturnType<typeof useGetProjectIsLockedLazyQuery>;
export type GetProjectIsLockedQueryResult = Apollo.QueryResult<GetProjectIsLockedQuery, GetProjectIsLockedQueryVariables>;
export function refetchGetProjectIsLockedQuery(variables: GetProjectIsLockedQueryVariables) {
return { query: GetProjectIsLockedDocument, variables: variables }
}
export const GetProjectLocalesDocument = gql`
query getProjectLocales($appId: uuid!) {
config(appID: $appId, resolve: false) {
@@ -25313,6 +25506,161 @@ export type GetConfigRawJsonQueryResult = Apollo.QueryResult<GetConfigRawJsonQue
export function refetchGetConfigRawJsonQuery(variables: GetConfigRawJsonQueryVariables) {
return { query: GetConfigRawJsonDocument, variables: variables }
}
export const GetRateLimitConfigDocument = gql`
query getRateLimitConfig($appId: uuid!, $resolve: Boolean!) {
config(appID: $appId, resolve: $resolve) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}
`;
/**
* __useGetRateLimitConfigQuery__
*
* To run a query within a React component, call `useGetRateLimitConfigQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRateLimitConfigQuery` 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 } = useGetRateLimitConfigQuery({
* variables: {
* appId: // value for 'appId'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetRateLimitConfigQuery(baseOptions: Apollo.QueryHookOptions<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>(GetRateLimitConfigDocument, options);
}
export function useGetRateLimitConfigLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>(GetRateLimitConfigDocument, options);
}
export type GetRateLimitConfigQueryHookResult = ReturnType<typeof useGetRateLimitConfigQuery>;
export type GetRateLimitConfigLazyQueryHookResult = ReturnType<typeof useGetRateLimitConfigLazyQuery>;
export type GetRateLimitConfigQueryResult = Apollo.QueryResult<GetRateLimitConfigQuery, GetRateLimitConfigQueryVariables>;
export function refetchGetRateLimitConfigQuery(variables: GetRateLimitConfigQueryVariables) {
return { query: GetRateLimitConfigDocument, variables: variables }
}
export const UpdateRateLimitConfigDocument = gql`
mutation UpdateRateLimitConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
updateConfig(appID: $appId, config: $config) {
hasura {
rateLimit {
limit
interval
}
}
storage {
rateLimit {
limit
interval
}
}
functions {
rateLimit {
limit
interval
}
}
auth {
rateLimit {
bruteForce {
limit
interval
}
emails {
limit
interval
}
global {
limit
interval
}
signups {
limit
interval
}
sms {
limit
interval
}
}
}
}
}
`;
export type UpdateRateLimitConfigMutationFn = Apollo.MutationFunction<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>;
/**
* __useUpdateRateLimitConfigMutation__
*
* To run a mutation, you first call `useUpdateRateLimitConfigMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateRateLimitConfigMutation` 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 [updateRateLimitConfigMutation, { data, loading, error }] = useUpdateRateLimitConfigMutation({
* variables: {
* appId: // value for 'appId'
* config: // value for 'config'
* },
* });
*/
export function useUpdateRateLimitConfigMutation(baseOptions?: Apollo.MutationHookOptions<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>(UpdateRateLimitConfigDocument, options);
}
export type UpdateRateLimitConfigMutationHookResult = ReturnType<typeof useUpdateRateLimitConfigMutation>;
export type UpdateRateLimitConfigMutationResult = Apollo.MutationResult<UpdateRateLimitConfigMutation>;
export type UpdateRateLimitConfigMutationOptions = Apollo.BaseMutationOptions<UpdateRateLimitConfigMutation, UpdateRateLimitConfigMutationVariables>;
export const ReplaceConfigRawJsonDocument = gql`
mutation ReplaceConfigRawJSON($appID: uuid!, $rawJSON: String!) {
replaceConfigRawJSON(appID: $appID, rawJSON: $rawJSON)
@@ -25814,6 +26162,38 @@ export function useUpdateConfigMutation(baseOptions?: Apollo.MutationHookOptions
export type UpdateConfigMutationHookResult = ReturnType<typeof useUpdateConfigMutation>;
export type UpdateConfigMutationResult = Apollo.MutationResult<UpdateConfigMutation>;
export type UpdateConfigMutationOptions = Apollo.BaseMutationOptions<UpdateConfigMutation, UpdateConfigMutationVariables>;
export const UpdateDatabaseVersionDocument = gql`
mutation UpdateDatabaseVersion($appId: uuid!, $version: String!) {
changeDatabaseVersion(appID: $appId, version: $version)
}
`;
export type UpdateDatabaseVersionMutationFn = Apollo.MutationFunction<UpdateDatabaseVersionMutation, UpdateDatabaseVersionMutationVariables>;
/**
* __useUpdateDatabaseVersionMutation__
*
* To run a mutation, you first call `useUpdateDatabaseVersionMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateDatabaseVersionMutation` 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 [updateDatabaseVersionMutation, { data, loading, error }] = useUpdateDatabaseVersionMutation({
* variables: {
* appId: // value for 'appId'
* version: // value for 'version'
* },
* });
*/
export function useUpdateDatabaseVersionMutation(baseOptions?: Apollo.MutationHookOptions<UpdateDatabaseVersionMutation, UpdateDatabaseVersionMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateDatabaseVersionMutation, UpdateDatabaseVersionMutationVariables>(UpdateDatabaseVersionDocument, options);
}
export type UpdateDatabaseVersionMutationHookResult = ReturnType<typeof useUpdateDatabaseVersionMutation>;
export type UpdateDatabaseVersionMutationResult = Apollo.MutationResult<UpdateDatabaseVersionMutation>;
export type UpdateDatabaseVersionMutationOptions = Apollo.BaseMutationOptions<UpdateDatabaseVersionMutation, UpdateDatabaseVersionMutationVariables>;
export const UnpauseApplicationDocument = gql`
mutation UnpauseApplication($appId: uuid!) {
updateApp(pk_columns: {id: $appId}, _set: {desiredState: 5}) {
@@ -26401,6 +26781,48 @@ export type GetServiceLabelValuesQueryResult = Apollo.QueryResult<GetServiceLabe
export function refetchGetServiceLabelValuesQuery(variables: GetServiceLabelValuesQueryVariables) {
return { query: GetServiceLabelValuesDocument, variables: variables }
}
export const GetSystemLogsDocument = gql`
query getSystemLogs($appID: String!, $action: String!, $from: Timestamp, $to: Timestamp) {
systemLogs(appID: $appID, action: $action, from: $from) {
timestamp
log
}
}
`;
/**
* __useGetSystemLogsQuery__
*
* To run a query within a React component, call `useGetSystemLogsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetSystemLogsQuery` 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 } = useGetSystemLogsQuery({
* variables: {
* appID: // value for 'appID'
* action: // value for 'action'
* from: // value for 'from'
* to: // value for 'to'
* },
* });
*/
export function useGetSystemLogsQuery(baseOptions: Apollo.QueryHookOptions<GetSystemLogsQuery, GetSystemLogsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetSystemLogsQuery, GetSystemLogsQueryVariables>(GetSystemLogsDocument, options);
}
export function useGetSystemLogsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetSystemLogsQuery, GetSystemLogsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetSystemLogsQuery, GetSystemLogsQueryVariables>(GetSystemLogsDocument, options);
}
export type GetSystemLogsQueryHookResult = ReturnType<typeof useGetSystemLogsQuery>;
export type GetSystemLogsLazyQueryHookResult = ReturnType<typeof useGetSystemLogsLazyQuery>;
export type GetSystemLogsQueryResult = Apollo.QueryResult<GetSystemLogsQuery, GetSystemLogsQueryVariables>;
export function refetchGetSystemLogsQuery(variables: GetSystemLogsQueryVariables) {
return { query: GetSystemLogsDocument, variables: variables }
}
export const DeletePaymentMethodDocument = gql`
mutation deletePaymentMethod($paymentMethodId: uuid!) {
deletePaymentMethod(id: $paymentMethodId) {
@@ -27440,6 +27862,95 @@ export type GetLocalRunServiceConfigsQueryResult = Apollo.QueryResult<GetLocalRu
export function refetchGetLocalRunServiceConfigsQuery(variables: GetLocalRunServiceConfigsQueryVariables) {
return { query: GetLocalRunServiceConfigsDocument, variables: variables }
}
export const GetRunServicesRateLimitDocument = gql`
query getRunServicesRateLimit($appID: uuid!, $resolve: Boolean!) {
app(id: $appID) {
runServices {
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
...RunServiceRateLimit
}
}
}
}
${RunServiceRateLimitFragmentDoc}`;
/**
* __useGetRunServicesRateLimitQuery__
*
* To run a query within a React component, call `useGetRunServicesRateLimitQuery` and pass it any options that fit your needs.
* When your component renders, `useGetRunServicesRateLimitQuery` 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 } = useGetRunServicesRateLimitQuery({
* variables: {
* appID: // value for 'appID'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetRunServicesRateLimitQuery(baseOptions: Apollo.QueryHookOptions<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>(GetRunServicesRateLimitDocument, options);
}
export function useGetRunServicesRateLimitLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>(GetRunServicesRateLimitDocument, options);
}
export type GetRunServicesRateLimitQueryHookResult = ReturnType<typeof useGetRunServicesRateLimitQuery>;
export type GetRunServicesRateLimitLazyQueryHookResult = ReturnType<typeof useGetRunServicesRateLimitLazyQuery>;
export type GetRunServicesRateLimitQueryResult = Apollo.QueryResult<GetRunServicesRateLimitQuery, GetRunServicesRateLimitQueryVariables>;
export function refetchGetRunServicesRateLimitQuery(variables: GetRunServicesRateLimitQueryVariables) {
return { query: GetRunServicesRateLimitDocument, variables: variables }
}
export const GetLocalRunServiceRateLimitDocument = gql`
query getLocalRunServiceRateLimit($appID: uuid!, $resolve: Boolean!) {
runServiceConfigs(appID: $appID, resolve: $resolve) {
serviceID
config {
...RunServiceRateLimit
}
}
}
${RunServiceRateLimitFragmentDoc}`;
/**
* __useGetLocalRunServiceRateLimitQuery__
*
* To run a query within a React component, call `useGetLocalRunServiceRateLimitQuery` and pass it any options that fit your needs.
* When your component renders, `useGetLocalRunServiceRateLimitQuery` 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 } = useGetLocalRunServiceRateLimitQuery({
* variables: {
* appID: // value for 'appID'
* resolve: // value for 'resolve'
* },
* });
*/
export function useGetLocalRunServiceRateLimitQuery(baseOptions: Apollo.QueryHookOptions<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>(GetLocalRunServiceRateLimitDocument, options);
}
export function useGetLocalRunServiceRateLimitLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>(GetLocalRunServiceRateLimitDocument, options);
}
export type GetLocalRunServiceRateLimitQueryHookResult = ReturnType<typeof useGetLocalRunServiceRateLimitQuery>;
export type GetLocalRunServiceRateLimitLazyQueryHookResult = ReturnType<typeof useGetLocalRunServiceRateLimitLazyQuery>;
export type GetLocalRunServiceRateLimitQueryResult = Apollo.QueryResult<GetLocalRunServiceRateLimitQuery, GetLocalRunServiceRateLimitQueryVariables>;
export function refetchGetLocalRunServiceRateLimitQuery(variables: GetLocalRunServiceRateLimitQueryVariables) {
return { query: GetLocalRunServiceRateLimitDocument, variables: variables }
}
export const InsertRunServiceDocument = gql`
mutation insertRunService($object: run_service_insert_input!) {
insertRunService(object: $object) {

View File

@@ -1,5 +1,8 @@
import { ApplicationStatus } from '@/types/application';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import type {
ConfigRunServicePort,
DeploymentRowFragment,
} from '@/utils/__generated__/graphql';
import slugify from 'slugify';
export function getLastLiveDeployment(deployments?: DeploymentRowFragment[]) {
@@ -108,3 +111,22 @@ export const removeTypename = (obj: any) => {
});
return newObj;
};
export const getRunServicePortURL = (
subdomain: string,
regionName: string,
regionDomain: string,
port: Partial<ConfigRunServicePort>,
) => {
const { port: servicePort, ingresses } = port;
const customDomain = ingresses?.[0]?.fqdn?.[0];
if (customDomain) {
return `https://${customDomain}`;
}
const servicePortNumber =
Number(servicePort) > 0 ? Number(servicePort) : '[port]';
return `https://${subdomain}-${servicePortNumber}.svc.${regionName}.${regionDomain}`;
};

View File

@@ -1,5 +1,18 @@
# @nhost/docs
## 2.15.0
### Minor Changes
- 40c0d7b: │feat: added subdomain/region information
- a18b545: feat: added postgres upgrade docs
## 2.14.3
### Patch Changes
- 4564232: chore: update `clientStorage` docs and add usage examples
## 2.14.2
### Patch Changes

View File

@@ -1,121 +0,0 @@
---
title: Connect Devices to Local Nhost Project
description: Configuring dnsmasq for network device connectivity to a local Nhost project
icon: ethernet
---
## Introduction
If you want to connect to your local environment from other devices on the same network, such as Android emulators
or iPhone devices, you can use **dnsmasq**. Follow this guide for the necessary configuration steps to enable this
functionality for your local Nhost project running on your machine.
<Note>
Make sure to install **dnsmasq**. If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
brew install dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
apt-get install dnsmasq
```
</Tab>
<Tab title="Nix">
```shell Terminal
nix-env -iA nixpkgs.dnsmasq
```
</Tab>
</Tabs>
</Note>
# Configure dnsmasq for Android
<Warning>These steps are necessary when running on both an **Android emulator** or **physical Android device**</Warning>
<Steps>
<Step title="Configure dnsmasq">
Configure `dnsmasq` to resolve nhost service urls to your machine's special [loopback address](https://developer.android.com/studio/run/emulator-networking) `10.0.2.2`
```shell Terminal
sudo dnsmasq -d \
--address=/local.auth.nhost.run/10.0.2.2 \
--address=/local.graphql.nhost.run/10.0.2.2 \
--address=/local.storage.nhost.run/10.0.2.2 \
--address=/local.functions.nhost.run/10.0.2.2
```
</Step>
<Step title="Restart dnsmasq">
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
sudo brew services restart dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
sudo systemctl restart dnsmasq
```
</Tab>
</Tabs>
</Step>
<Step title="Configure the android device/emulator's DNS settings">
1. Edit your network settings: Settings > Network & Internet > Internet > AndroidWifi
2. set `IP settings` to `Static`
3. set `DNS 1` and `DNS 2` to `10.0.2.2`
4. Save
</Step>
</Steps>
# Configure dnsmasq for iOS
<Warning>These steps are only necessary when running on physical iOS device, the iOS simulator uses the host machine's network, so no additional configuration is typically needed.</Warning>
<Steps>
<Step title="Inspect your machine's IP address on your network">
<Tabs>
<Tab title="macOS">
```shell Terminal
ipconfig getifaddr en0
```
</Tab>
<Tab title="Debian">
```shell Terminal
ip addr show dev en0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1
```
</Tab>
</Tabs>
</Step>
<Step title="Configure dnsmasq">
Configure `dnsmasq` to resolve nhost service urls to your machine's ip address.
<Warning>Make sure to replace every occurrence of **[your-machine-s-up-address]** with the address printed in Step `1`</Warning>
```shell Terminal
sudo dnsmasq -d \
--address=/local.auth.nhost.run/[your-machine-s-up-address] \
--address=/local.graphql.nhost.run/[your-machine-s-up-address] \
--address=/local.storage.nhost.run/[your-machine-s-up-address] \
--address=/local.functions.nhost.run/[your-machine-s-up-address]
```
</Step>
<Step title="Restart dnsmasq">
If you're using another OS, please refer to the [dnsmasq website](https://thekelleys.org.uk/dnsmasq/doc.html).
<Tabs>
<Tab title="macOS">
```shell Terminal
sudo brew services restart dnsmasq
```
</Tab>
<Tab title="Debian">
```shell Terminal
sudo systemctl restart dnsmasq
```
</Tab>
</Tabs>
</Step>
<Step title="Configure the iPhone's DNS settings">
1. Select the wifi you're connected and select `Configure DNS`
2. Select `Manual`
3. Click on `Add server` and type your local machine's IP address printed in Step `1`
4. Save
</Step>
</Steps>

View File

@@ -0,0 +1,119 @@
---
title: Subdomain/Region
description: Connecting to your local environment
icon: compass
---
When you start the CLI the services are exposed similarly to the way they are exposed in the [cloud](/platform/subdomain). For instance, the following information is shown on your terminal after running `nhost up`
```
> nhost up
...
URLs:
- Postgres: postgres://postgres:postgres@localhost:5432/local
- Hasura: https://local.hasura.local.nhost.run
- GraphQL: https://local.graphql.local.nhost.run
- Auth: https://local.auth.local.nhost.run
- Storage: https://local.storage.local.nhost.run
- Functions: https://local.functions.local.nhost.run
- Dashboard: https://local.dashboard.local.nhost.run
- Mailhog: https://local.mailhog.local.nhost.run
SDK Configuration:
Subdomain: local
Region: local
```
There you can see the various URLs you can use to access each service plus the region and subdomain you can use to configure the SDK:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'local', subdomain: 'local' }
)
```
The domains in the URLs above will all return the IP address for localhost, `127.0.0.1`, which should suffice for most development environments. For instance:
```
> host local.auth.local.nhost.run
local.auth.local.nhost.run has address 127.0.0.1
```
However, those URLs are powered by a dynamic DNS that can return any IPv4 address you need, you just need to replace the subdomain `local` with a `subdomain` that contains the 4 octets of the IPv4 adress you want separated by `-`. For instance:
```
> host 192-168-100-1.auth.local.nhost.run
192-168-100-1.auth.local.nhost.run has address 192.168.100.1
> host 10-10-1-108.auth.local.nhost.run
10-10-1-108.auth.local.nhost.run has address 10.10.1.108
```
This is useful if you need to connect to your environment from a different device, a VM or a mobile device emulator.
To make use of this functionality you can start your development environment after setting the environment variable `NHOST_LOCAL_SUBDOMAIN` or passing the flag `--local-subdomain` :
```
> export NHOST_LOCAL_SUBDOMAIN=192-168-1-1-8 # either this or --local-subdomain 192-168-1-108
> nhost --local-subdomain 192-168-1-108 up
...
Nhost development environment started.
URLs:
- Postgres: postgres://postgres:postgres@localhost:5432/local
- Hasura: https://192-168-1-108.hasura.local.nhost.run
- GraphQL: https://192-168-1-108.graphql.local.nhost.run
- Auth: https://192-168-1-108.auth.local.nhost.run
- Storage: https://192-168-1-108.storage.local.nhost.run
- Functions: https://192-168-1-108.functions.local.nhost.run
- Dashboard: https://192-168-1-108.dashboard.local.nhost.run
- Mailhog: https://192-168-1-108.mailhog.local.nhost.run
SDK Configuration:
Subdomain: 192-168-1-108
Region: local
Run `nhost up` to reload the development environment
Run `nhost down` to stop the development environment
Run `nhost logs` to watch the logs
```
Now you can configure the SDK with:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'local', subdomain: '192-168-1-108' }
)
```
<Warning>
If you are trying to connect to your local environment from an external device or VM make sure that:
- The IP address you are using is reachable from this device/VM
- That your firewall isn't blocking requests
</Warning>
<Warning>
If you are testing a social provider don't forget you will need to configure the callback URL to match the subdomain/region you are using. The dashboard should be able to provide this information in settings page.
</Warning>
## Offline access
All the URLs in this document are resolved by a public DNS, which means you need Internet access to resolve them. If you need to use any of those URLs without Internet access you can add them to your `/etc/hosts` file. For instance:
```
> cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
# ::1 localhost
127.0.0.1 local.auth.local.nhost.run local.storage.local.nhost.run ...
```
Just start with the IP you want to resolve followed by all the entries you need separated by spaces.

View File

@@ -0,0 +1,37 @@
---
title: "Upgrade Major Version"
description: Upgrade to Postgres 15.x or 16.y
icon: circle-up
---
# Upgrade process
<Info>
This document only applies when changing Postgres major version (i.e. from 14 to 15/16 or from 15 to 16). It doesn'e apply when upgrading minor versions (i.e. from 14.5 to 14.11).
</Info>
While new cloud projects ship with Postgres 14 by default, versions 15 and 16 are also supported. To change your major version you can go to Settings -> Database, select the new major version and start the process:
![dashboard settings](/images/guides/database/upgrade_01.png)
<Warning>
Keep in mind that the upgrade process requires downtime. Pay attention to all the information provided to you in the settings page.
</Warning>
After starting the process you can follow it on the same page:
![logs](/images/guides/database/upgrade_02.png)
Finally, you can confirm the upgrade by executing the SQL query `SELECT version();`
![select version()](/images/guides/database/upgrade_03.png)
## Projects with connected repos
This process can only be triggered from the dashboard. If you have a project with a connected repository and want to upgrade postgres to either 15 or 16 you will have to follow the steps below:
1. Upgrade the major version using the dashboard
2. Run `nhost config pull` or edit the `nhost.toml` by hand.
3. Push to git (this step should be a NOOP and can be skipped)
If you attempt to change major versions via a deployment the deployment will fail. This is done on purpose to avoid unintended upgrades which can lead to downtime.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

View File

@@ -73,6 +73,7 @@
{
"group": "Platform",
"pages": [
"platform/subdomain",
"platform/compute-resources",
"platform/service-replicas",
{
@@ -117,7 +118,8 @@
"guides/database/configuring-postgres",
"guides/database/access",
"guides/database/extensions",
"guides/database/performance"
"guides/database/performance",
"guides/database/upgrade-major"
]
},
{
@@ -197,11 +199,11 @@
"group": "CLI",
"pages": [
"guides/cli/local-development",
"guides/cli/subdomain",
"guides/cli/migrate-config",
"guides/cli/multiple-projects",
"guides/cli/configuration-overlays",
"guides/cli/seeds",
"guides/cli/connect-devices-to-local-nhost-project"
"guides/cli/seeds"
]
},
{

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "2.14.2",
"version": "2.15.0",
"private": true,
"scripts": {
"start": "mintlify dev"

View File

@@ -0,0 +1,40 @@
---
title: Subdomain/Region
description: Connecting to your Cloud project
icon: compass
---
Services in the Nhost Cloud adhere to the following format:
- https://&lt;subdomain&gt;.&lt;service&gt;.&lt;region&gt;.nhost.run
When you set up a new project in Nhost Cloud, you're assigned a subdomain that, along with the region where the project was created, allows you to generate the different URLs for your services. For example:
![dashboard](/images/platform/subdomain.png)
With that information at hand generating the URLs for the various services is trivial:
- https://xglwkhjnufblhgtwtfwz.auth.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.db.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.functions.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.graphql.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.hasura.us-west-2.nhost.run
- https://xglwkhjnufblhgtwtfwz.storage.us-west-2.nhost.run
If you are using our SDK you can just specify the region and subdomain and the SDK will automatically generate these URLs for you. For instance:
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient(
{ region: 'xglwkhjnufblhgtwtfwz', subdomain: 'us-west-2' }
)
```
<Note>
If you want to use your own domain you can head to the [custom domains documentation](/platform/custom-domains)
</Note>
<Note>
For information on how to access your local environment check the [cli documentation](/guides/cli/subdomain)
</Note>

View File

@@ -39,14 +39,63 @@ Object where the refresh token will be persisted and read locally.
Recommended values:
- `'web'` and `'cookies'`: no value is required
- `'react-native'`: `import Storage from @react-native-async-storage/async-storage`
- `'cookies'`: `localStorage`
- `'react-native'`: use [@react-native-async-storage/async-storage](https://www.npmjs.com/package/@react-native-async-storage/async-storage)
```ts
import { NhostClient } from '@nhost/nhost-js'
import AsyncStorage from '@react-native-async-storage/async-storage';
const nhost = new NhostClient({
...
clientStorageType: 'react-native',
clientStorage: AsyncStorage
})
```
- `'custom'`: an object that defines the following methods:
- `setItem` or `setItemAsync`
- `getItem` or `getItemAsync`
- `removeItem`
- `'capacitor'`: `import { Storage } from @capacitor/storage`
- `'expo-secure-store'`: `import * as SecureStore from 'expo-secure-store'`
- `'capacitor'`:
- capacitor version **< 4** : use [@capacitor/storage](https://www.npmjs.com/package/@capacitor/storage)
```ts
import { NhostClient } from '@nhost/nhost-js'
import { Storage } from '@capacitor/storage'
const nhost = new NhostClient({
...
clientStorageType: 'capacitor',
clientStorage: Storage
})
```
- capacitor version **>= 4** : use [@capacitor/preferences](https://www.npmjs.com/package/@capacitor/preferences)
```ts
import { NhostClient } from '@nhost/nhost-js';
import { Preferences } from '@capacitor/preferences';
const nhost = new NhostClient({
...
clientStorageType: 'custom',
clientStorage: {
setItemAsync: async (key, value) => Preferences.set({ key, value }),
getItemAsync: async (key) => {
const { value } = await Preferences.get({ key });
return value;
},
removeItem(key): (key) => Preferences.remove({ key })
},
});
```
- `'expo-secure-store'`: use [expo-secure-store](https://www.npmjs.com/package/expo-secure-store)
```ts
import { NhostClient } from '@nhost/nhost-js'
import * as SecureStore from 'expo-secure-store';
const nhost = new NhostClient({
...
clientStorageType: 'expo-secure-store',
clientStorage: SecureStore
})
```
| Property | Type | Required | Notes |
| :-------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :------: | :---- |

View File

@@ -22,7 +22,7 @@ const nhost = new NhostClient({
```ts
// Create a new Nhost client for local development.
const nhost = new NhostClient({ subdomain: 'local' })
const nhost = new NhostClient({ region: 'local', subdomain: 'local' })
```
## Parameters

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