Compare commits

..

8 Commits

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


# Releases
## @nhost/dashboard@1.22.0

### Minor Changes

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

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

### Patch Changes

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

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


___

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



___



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

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

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


</details>


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

</tr>                    

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

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

- Added new `CommunityIcon` component.



</details>


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

</tr>                    

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

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

- Exported `CommunityIcon` component.



</details>


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

</tr>                    

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

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

- Added new `DiscordIcon` component.



</details>


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

</tr>                    

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

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

- Exported `DiscordIcon` component.



</details>


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

</tr>                    

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

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

- Added new `EnvelopeIcon` component.



</details>


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

</tr>                    

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

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

- Exported `EnvelopeIcon` component.



</details>


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

</tr>                    

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

dashboard/src/pages/support/index.tsx

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


</details>


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

</tr>                    

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

dashboard/src/pages/support/ticket.tsx

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


</details>


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

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

dashboard/.env.example

- Added environment variables for Zendesk integration.



</details>


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

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

___

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

---------

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


___

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


___

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



___



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

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

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



</details>


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

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

examples/react-apollo/e2e/utils.ts

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


</details>


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

</tr>                    

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

examples/react-apollo/src/Verify.tsx

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


</details>


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

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

.changeset/gentle-brooms-flash.md

- Added changeset for email verification button update.



</details>


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

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

examples/react-apollo/nhost/nhost.toml

- Updated Hasura, Auth, and Postgres versions.



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-05 15:31:15 +01:00
Zephyr (David B.M.)
807b8c049a fix (dashboard): show city name in region selector for project creation (#2788)
Fixes #2778
2024-07-05 12:46:38 +02:00
Zephyr (David B.M.)
998c0376bf fix (dashboard): align dropdown items in select component (#2786)
Fixes #2779
2024-07-05 12:02:18 +02:00
github-actions[bot]
cf5423dac6 chore: update versions (#2785)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.21.0

### Minor Changes

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

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


___

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



___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Configuration
changes</strong></td><td><table>
<tr>
  <td>
    <details>
<summary><strong>action.yaml</strong><dd><code>Update pnpm/action-setup
to v4 in GitHub Actions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></summary>
<hr>

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

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


</details>


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

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

package.json

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



</details>


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

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

___

> 💡 **PR-Agent usage**:
>Comment `/help` on the PR to get a list of all available PR-Agent tools
and their descriptions
2024-07-04 15:26:23 +01:00
44 changed files with 1462 additions and 633 deletions

View File

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

View File

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

View File

@@ -1,5 +1,18 @@
# @nhost/dashboard
## 1.22.0
### Minor Changes
- 998c037: fix: align drop-down list in select component
- 807b8c0: fix: show city name in region selection for project creation
## 1.21.0
### Minor Changes
- a2efeed: fix: improve project health error handling, add unknown state and polling interval for health state
## 1.20.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.20.0",
"version": "1.22.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -48,6 +48,7 @@
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.15.3",
"@tanstack/react-virtual": "^3.2.0",
"@uidotdev/usehooks": "^2.4.1",
"@uiw/codemirror-theme-bbedit": "^4.22.2",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",

View File

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

View File

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

View File

@@ -37,6 +37,9 @@ const Badge = styled(MaterialBadge)<BadgeProps>(({ theme }) => ({
'& .MuiBadge-colorSuccess': {
backgroundColor: theme.palette.success.dark,
},
'& .MuiBadge-colorSecondary': {
backgroundColor: theme.palette.grey[500],
},
}));
Badge.displayName = 'NhostBadge';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
@@ -9,105 +7,45 @@ import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
import { useSoftwareVersionsInfo } from '@/features/projects/common/hooks/useSoftwareVersionsInfo';
import { OverviewProjectHealthModal } from '@/features/projects/overview/components/OverviewProjectHealthModal';
import { ProjectHealthCard } from '@/features/projects/overview/components/ProjectHealthCard';
import { RunStatusTooltip } from '@/features/projects/overview/components/RunStatusTooltip';
import { ServiceVersionTooltip } from '@/features/projects/overview/components/ServiceVersionTooltip';
import {
baseServices,
findHighestImportanceState,
serviceStateToThemeColor,
type ServiceHealthInfo,
} from '@/features/projects/overview/health';
import {
ServiceState,
useGetConfiguredVersionsQuery,
useGetProjectServicesHealthQuery,
useGetRecommendedSoftwareVersionsQuery,
} from '@/generated/graphql';
interface RunStatusTooltipProps {
servicesStatusInfo?: Array<ServiceHealthInfo>;
openHealthModal?: (
defaultExpanded?: keyof typeof baseServices | 'run',
) => void;
}
function RunStatusTooltip({
servicesStatusInfo,
openHealthModal,
}: RunStatusTooltipProps) {
return (
<div className="flex w-full flex-col gap-3 px-2 py-3">
<ol className="m-0 flex flex-col gap-3">
{servicesStatusInfo.map((service) => (
<li
key={service.name}
className="flex flex-row items-center gap-4 text-ellipsis text-nowrap leading-5"
>
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(service.state),
}}
className={`h-3 w-3 flex-shrink-0 rounded-full ${
service.state === ServiceState.Updating ? 'animate-pulse' : ''
}`}
/>
<Text
sx={{
color: (theme) =>
theme.palette.mode === 'dark'
? 'text.primary'
: 'text.primary',
}}
className="font-semibold"
>
{service.name}
</Text>
</li>
))}
</ol>
<Button variant="outlined" onClick={() => openHealthModal('run')}>
View state
</Button>
</div>
);
}
export default function OverviewProjectHealth() {
const isPlatform = useIsPlatform();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data: recommendedVersionsData, loading: loadingRecommendedVersions } =
useGetRecommendedSoftwareVersionsQuery({
skip: !isPlatform || !currentProject,
});
const { openDialog, closeDialog } = useDialog();
const { data: configuredVersionsData, loading: loadingConfiguredVersions } =
useGetConfiguredVersionsQuery({
variables: {
appId: currentProject?.id,
},
skip: !isPlatform || !currentProject,
});
const {
loading: loadingVersions,
auth: authVersionInfo,
storage: storageVersionInfo,
postgres: postgresVersionInfo,
hasura: hasuraVersionInfo,
ai: aiVersionInfo,
isAIEnabled,
} = useSoftwareVersionsInfo();
const {
data: projectServicesHealthData,
loading: loadingProjectServicesHealth,
} = useGetProjectServicesHealthQuery({
variables: {
appId: currentProject?.id,
},
skip: !isPlatform || !currentProject,
auth: authStatus,
storage: storageStatus,
postgres: postgresStatus,
hasura: hasuraStatus,
ai: aiStatus,
run: runStatus,
} = useServiceStatus({
shouldPoll: true,
});
if (
loadingRecommendedVersions ||
loadingConfiguredVersions ||
loadingProjectServicesHealth
) {
if (loadingVersions || loadingProjectServicesHealth) {
return (
<div className="grid grid-flow-row content-start gap-6">
<Text variant="h3">Project Health</Text>
@@ -133,78 +71,12 @@ export default function OverviewProjectHealth() {
);
}
const isAIServiceEnabled = !!configuredVersionsData?.config?.ai;
const getRecommendedVersions = (softwareName: string): string[] =>
recommendedVersionsData?.softwareVersions.reduce(
(recommendedVersions, service) => {
if (service.software === softwareName) {
recommendedVersions.push(service.version);
}
return recommendedVersions;
},
[],
) ?? [];
const authRecommendedVersions = getRecommendedVersions(
baseServices['hasura-auth'].softwareVersionsName,
);
const hasuraRecommendedVersions = getRecommendedVersions(
baseServices.hasura.softwareVersionsName,
);
const postgresRecommendedVersions = getRecommendedVersions(
baseServices.postgres.softwareVersionsName,
);
const storageRecommendedVersions = getRecommendedVersions(
baseServices['hasura-storage'].softwareVersionsName,
);
const aiRecommendedVersions = getRecommendedVersions(
baseServices.ai.softwareVersionsName,
);
// Check if configured version can't be found in recommended versions
const isAuthVersionMismatch = !authRecommendedVersions.find(
(version) => configuredVersionsData?.config?.auth?.version === version,
);
const isHasuraVersionMismatch = !hasuraRecommendedVersions.find(
(version) => configuredVersionsData?.config?.hasura?.version === version,
);
const isPostgresVersionMismatch = !postgresRecommendedVersions.find(
(version) => configuredVersionsData?.config?.postgres?.version === version,
);
const isStorageVersionMismatch = !storageRecommendedVersions.find(
(version) => configuredVersionsData?.config?.storage?.version === version,
);
const isAIVersionMismatch = !aiRecommendedVersions.find(
(version) => configuredVersionsData?.config?.ai?.version === version,
);
const serviceMap: { [key: string]: ServiceHealthInfo | undefined } = {};
projectServicesHealthData?.getProjectStatus?.services.forEach((service) => {
serviceMap[service.name] = service;
});
const {
'hasura-auth': authStatus,
'hasura-storage': storageStatus,
postgres: postgresStatus,
hasura: hasuraStatus,
ai: aiStatus,
...otherServicesStatus
} = serviceMap;
const openHealthModal = async (
defaultExpanded: keyof typeof baseServices | 'run',
) => {
openDialog({
component: (
<OverviewProjectHealthModal
servicesHealth={projectServicesHealthData}
defaultExpanded={defaultExpanded}
/>
<OverviewProjectHealthModal defaultExpanded={defaultExpanded} />
),
props: {
PaperProps: { className: 'p-0 max-w-2xl w-full' },
@@ -220,9 +92,9 @@ export default function OverviewProjectHealth() {
<ServiceVersionTooltip
serviceName={baseServices['hasura-auth'].displayName}
serviceKey="hasura-auth"
usedVersion={configuredVersionsData?.config?.auth?.version ?? ''}
recommendedVersionMismatch={isAuthVersionMismatch}
recommendedVersions={authRecommendedVersions}
usedVersion={authVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={authVersionInfo?.isVersionMismatch}
recommendedVersions={authVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={authStatus?.state}
/>
@@ -232,9 +104,9 @@ export default function OverviewProjectHealth() {
<ServiceVersionTooltip
serviceName={baseServices.hasura.displayName}
serviceKey="hasura"
usedVersion={configuredVersionsData?.config?.hasura?.version ?? ''}
recommendedVersionMismatch={isHasuraVersionMismatch}
recommendedVersions={hasuraRecommendedVersions}
usedVersion={hasuraVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
recommendedVersions={hasuraVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={hasuraStatus?.state}
/>
@@ -244,9 +116,9 @@ export default function OverviewProjectHealth() {
<ServiceVersionTooltip
serviceName={baseServices.postgres.displayName}
serviceKey="postgres"
usedVersion={configuredVersionsData?.config?.postgres?.version ?? ''}
recommendedVersionMismatch={isPostgresVersionMismatch}
recommendedVersions={postgresRecommendedVersions}
usedVersion={postgresVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={postgresVersionInfo?.isVersionMismatch}
recommendedVersions={postgresVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={postgresStatus?.state}
/>
@@ -256,9 +128,9 @@ export default function OverviewProjectHealth() {
<ServiceVersionTooltip
serviceName={baseServices['hasura-storage'].displayName}
serviceKey="hasura-storage"
usedVersion={configuredVersionsData?.config?.storage?.version ?? ''}
recommendedVersionMismatch={isStorageVersionMismatch}
recommendedVersions={storageRecommendedVersions}
usedVersion={storageVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={storageVersionInfo?.isVersionMismatch}
recommendedVersions={storageVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={storageStatus?.state}
/>
@@ -268,16 +140,16 @@ export default function OverviewProjectHealth() {
<ServiceVersionTooltip
serviceName={baseServices.ai.displayName}
serviceKey="ai"
usedVersion={configuredVersionsData?.config?.ai?.version ?? ''}
recommendedVersionMismatch={isAIVersionMismatch}
recommendedVersions={aiRecommendedVersions}
usedVersion={aiVersionInfo?.configuredVersion ?? ''}
recommendedVersionMismatch={aiVersionInfo?.isVersionMismatch}
recommendedVersions={aiVersionInfo?.recommendedVersions}
openHealthModal={openHealthModal}
state={aiStatus?.state}
/>
);
const runServices = Object.values(otherServicesStatus).filter((service) =>
service.name.startsWith('run-'),
const runServices = Object.values(runStatus).filter((service) =>
service?.name?.startsWith('run-'),
);
const runServicesStates = runServices.map((service) => service.state);
@@ -293,32 +165,32 @@ export default function OverviewProjectHealth() {
<ProjectHealthCard
icon={<UserIcon className="m-1 h-6 w-6" />}
tooltip={authTooltipElem}
isVersionMismatch={isAuthVersionMismatch}
isVersionMismatch={authVersionInfo?.isVersionMismatch}
state={authStatus?.state}
/>
<ProjectHealthCard
icon={<DatabaseIcon className="m-1 h-6 w-6" />}
tooltip={postgresTooltipElem}
isVersionMismatch={isPostgresVersionMismatch}
isVersionMismatch={postgresVersionInfo?.isVersionMismatch}
state={postgresStatus?.state}
/>
<ProjectHealthCard
icon={<StorageIcon className="m-1 h-6 w-6" />}
tooltip={storageTooltipElem}
isVersionMismatch={isStorageVersionMismatch}
isVersionMismatch={storageVersionInfo?.isVersionMismatch}
state={storageStatus?.state}
/>
<ProjectHealthCard
icon={<HasuraIcon className="m-1 h-6 w-6" />}
tooltip={hasuraTooltipElem}
isVersionMismatch={isHasuraVersionMismatch}
isVersionMismatch={hasuraVersionInfo?.isVersionMismatch}
state={hasuraStatus?.state}
/>
{isAIServiceEnabled && (
{isAIEnabled && (
<ProjectHealthCard
icon={<AIIcon className="m-1 h-6 w-6" />}
tooltip={aiTooltipElem}
isVersionMismatch={isAIVersionMismatch}
isVersionMismatch={aiVersionInfo?.isVersionMismatch}
state={aiStatus?.state}
/>
)}

View File

@@ -1,225 +1,33 @@
import { CodeBlock } from '@/components/presentational/CodeBlock';
import { Accordion } from '@/components/ui/v2/Accordion';
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
import { ServicesOutlinedIcon } from '@/components/ui/v2/icons/ServicesOutlinedIcon';
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { useServiceStatus } from '@/features/projects/common/hooks/useServiceStatus';
import { ServiceAccordion } from '@/features/projects/overview/components/ServiceAccordion';
import {
findHighestImportanceState,
serviceStateToThemeColor,
type baseServices,
type ServiceHealthInfo,
} from '@/features/projects/overview/health';
import { removeTypename } from '@/utils/helpers';
import {
ServiceState,
type GetProjectServicesHealthQuery,
} from '@/utils/__generated__/graphql';
import Image from 'next/image';
import { type ReactElement } from 'react';
import { twMerge } from 'tailwind-merge';
interface ServiceAccordionProps {
serviceName: string;
serviceHealth: ServiceHealthInfo;
replicas: ServiceHealthInfo['replicas'];
serviceState: ServiceState;
/**
* Icon to display on the accordion.
*/
icon?: string | ReactElement;
/**
* Label of the icon.
*/
alt?: string;
iconIsComponent?: boolean;
defaultExpanded?: boolean;
}
function ServiceAccordion({
serviceName,
serviceHealth,
replicas,
serviceState,
icon,
iconIsComponent = true,
alt,
defaultExpanded = false,
}: ServiceAccordionProps) {
const replicasLabel = replicas.length === 1 ? 'replica' : 'replicas';
const serviceInfo = removeTypename(serviceHealth);
const blink = serviceState === ServiceState.Updating;
return (
<Accordion.Root defaultExpanded={defaultExpanded}>
<Accordion.Summary
expandIcon={
<ChevronDownIcon
sx={{
color: 'text.primary',
}}
/>
}
aria-controls="panel1-content"
id="panel1-header"
className="px-6"
>
<div className="flex flex-row justify-between gap-2 py-2">
<div className="flex items-center gap-3">
{iconIsComponent
? icon
: typeof icon === 'string' && <Image src={icon} alt={alt} />}
<Text
sx={{ color: 'text.primary' }}
variant="h4"
className="font-semibold"
>
{serviceName}{' '}
<Text
sx={{
color: 'text.secondary',
}}
component="span"
className="font-semibold"
>
({replicas.length} {replicasLabel})
</Text>
</Text>
{serviceState === ServiceState.Running ? (
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(serviceState),
}}
className="flex h-2 w-2 items-center justify-center rounded-full"
>
<CheckIcon className="h-3/4 w-3/4 stroke-2 text-white" />
</Box>
) : (
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(serviceState),
}}
className={`h-2 w-2 rounded-full ${
blink ? 'animate-pulse' : ''
}`}
/>
)}
</div>
</div>
</Accordion.Summary>
<Accordion.Details>
<CodeBlock copyToClipboardToastTitle={`${serviceName} status`}>
{JSON.stringify(serviceInfo, null, 2)}
</CodeBlock>
</Accordion.Details>
</Accordion.Root>
);
}
interface RunServicesAccordionProps {
servicesHealth: Array<ServiceHealthInfo>;
serviceStates: ServiceState[];
/**
* Icon to display on the accordion.
*/
icon?: string | ReactElement;
/**
* Label of the icon.
*/
alt?: string;
iconIsComponent?: boolean;
defaultExpanded?: boolean;
}
function RunServicesAccordion({
serviceStates,
servicesHealth,
icon,
iconIsComponent = true,
defaultExpanded = false,
alt,
}: RunServicesAccordionProps) {
const globalState = findHighestImportanceState(serviceStates);
const serviceInfo = removeTypename(servicesHealth);
const blink = globalState === ServiceState.Updating;
return (
<Accordion.Root defaultExpanded={defaultExpanded}>
<Accordion.Summary
expandIcon={
<ChevronDownIcon
sx={{
color: 'text.primary',
}}
/>
}
aria-controls="panel1-content"
id="panel1-header"
className="px-6"
>
<div className="flex flex-row justify-between gap-2 py-2">
<div className="flex items-center gap-3">
{iconIsComponent
? icon
: typeof icon === 'string' && <Image src={icon} alt={alt} />}
<Text
sx={{ color: 'text.primary' }}
variant="h4"
className="font-semibold"
>
Run
</Text>
<Box
sx={{
backgroundColor: serviceStateToThemeColor.get(globalState),
}}
className={`h-2 w-2 rounded-full ${blink ? 'animate-pulse' : ''}`}
/>
</div>
</div>
</Accordion.Summary>
<Accordion.Details>
<CodeBlock copyToClipboardToastTitle="Run services status">
{JSON.stringify(serviceInfo, null, 2)}
</CodeBlock>
</Accordion.Details>
</Accordion.Root>
);
}
export interface OverviewProjectHealthModalProps {
servicesHealth?: GetProjectServicesHealthQuery;
defaultExpanded?: keyof typeof baseServices | 'run';
}
export default function OverviewProjectHealthModal({
servicesHealth,
defaultExpanded,
}: OverviewProjectHealthModalProps) {
const serviceMap: { [key: string]: ServiceHealthInfo | undefined } = {};
servicesHealth.getProjectStatus.services.forEach((service) => {
serviceMap[service.name] = service;
const { auth, storage, postgres, hasura, ai, run } = useServiceStatus({
fetchPolicy: 'cache-only',
shouldPoll: false,
});
const {
'hasura-auth': auth,
'hasura-storage': storage,
postgres,
hasura,
ai,
...otherServices
} = serviceMap;
const runServices = Object.values(otherServices).filter((service) =>
const runServices = Object.values(run).filter((service) =>
service.name.startsWith('run-'),
);
@@ -230,6 +38,24 @@ export default function OverviewProjectHealthModal({
const isAIExpandedByDefault = defaultExpanded === 'ai';
const isRunExpandedByDefault = defaultExpanded === 'run';
const getServiceInfo = (service) => {
const info = removeTypename(service);
return JSON.stringify(info, null, 2);
};
const serviceInfo = {
auth: getServiceInfo(auth),
storage: getServiceInfo(storage),
postgres: getServiceInfo(postgres),
hasura: getServiceInfo(hasura),
ai: getServiceInfo(ai),
run: getServiceInfo(Object.values(runServices)),
};
const runServicesState = findHighestImportanceState(
Object.values(runServices).map((service) => service.state),
);
return (
<Box className={twMerge('w-full rounded-lg pt-2 text-left')}>
<Box
@@ -242,8 +68,8 @@ export default function OverviewProjectHealthModal({
<ServiceAccordion
icon={<UserIcon className="h-4 w-4" />}
serviceName="Auth"
serviceHealth={auth}
replicas={auth?.replicas}
serviceInfo={serviceInfo.auth}
replicaCount={auth?.replicas?.length}
serviceState={auth?.state}
defaultExpanded={isAuthExpandedByDefault}
/>
@@ -251,8 +77,8 @@ export default function OverviewProjectHealthModal({
<ServiceAccordion
icon={<DatabaseIcon className="h-4 w-4" />}
serviceName="Postgres"
serviceHealth={postgres}
replicas={postgres?.replicas}
serviceInfo={serviceInfo.postgres}
replicaCount={postgres?.replicas?.length}
serviceState={postgres?.state}
defaultExpanded={isPostgresExpandedByDefault}
/>
@@ -260,8 +86,8 @@ export default function OverviewProjectHealthModal({
<ServiceAccordion
icon={<StorageIcon className="h-4 w-4" />}
serviceName="Storage"
serviceHealth={storage}
replicas={storage?.replicas}
serviceInfo={serviceInfo.storage}
replicaCount={storage?.replicas?.length}
serviceState={storage?.state}
defaultExpanded={isStorageExpandedByDefault}
/>
@@ -269,8 +95,8 @@ export default function OverviewProjectHealthModal({
<ServiceAccordion
icon={<HasuraIcon className="h-4 w-4" />}
serviceName="Hasura"
serviceHealth={hasura}
replicas={hasura?.replicas}
serviceInfo={serviceInfo.hasura}
replicaCount={hasura?.replicas?.length}
serviceState={hasura?.state}
defaultExpanded={isHasuraExpandedByDefault}
/>
@@ -280,22 +106,22 @@ export default function OverviewProjectHealthModal({
<ServiceAccordion
icon={<AIIcon className="h-4 w-4" />}
serviceName="AI"
serviceHealth={ai}
replicas={ai.replicas}
serviceState={ai.state}
serviceInfo={serviceInfo.ai}
replicaCount={ai?.replicas?.length}
serviceState={ai?.state}
defaultExpanded={isAIExpandedByDefault}
/>
</>
) : null}
{Object.values(runServices).length > 0 ? (
{runServices && Object.values(runServices).length > 0 ? (
<>
<Divider />
<RunServicesAccordion
servicesHealth={Object.values(runServices)}
<ServiceAccordion
icon={<ServicesOutlinedIcon className="h-4 w-4" />}
serviceStates={Object.values(runServices).map(
(service) => service.state,
)}
serviceName="Run"
serviceInfo={serviceInfo.run}
replicaCount={0}
serviceState={runServicesState}
defaultExpanded={isRunExpandedByDefault}
/>
</>

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import { Badge, type BadgeProps } from '@/components/ui/v2/Badge';
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
import { ExclamationFilledIcon } from '@/components/ui/v2/icons/ExclamationFilledIcon';
import { Tooltip, tooltipClasses } from '@/components/ui/v2/Tooltip';
import { ProjectHealthBadge } from '@/features/projects/overview/components/ProjectHealthBadge';
import { serviceStateToBadgeColor } from '@/features/projects/overview/health';
import { ServiceState } from '@/utils/__generated__/graphql';
import type { ImageProps } from 'next/image';
@@ -11,104 +9,6 @@ import Image from 'next/image';
import type { ReactElement } from 'react';
import { twMerge } from 'tailwind-merge';
interface HealthBadgeProps extends BadgeProps {
badgeVariant?: 'standard' | 'dot';
badgeColor?: 'success' | 'error' | 'warning';
showExclamation?: boolean;
showCheckIcon?: boolean;
isLoading?: boolean;
blink?: boolean;
}
function HealthBadge({
badgeColor,
badgeVariant,
showExclamation,
showCheckIcon,
blink,
children,
...props
}: HealthBadgeProps) {
if (!badgeColor) {
return <div>{children}</div>;
}
if (showExclamation) {
return (
<Badge
variant="standard"
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
badgeContent={
<ExclamationFilledIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'grey.600',
}}
className="h-2.5 w-2.5"
/>
}
>
<Badge
color={badgeColor}
variant={badgeVariant}
badgeContent={
showCheckIcon ? (
<CheckIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-2 w-2 stroke-2"
/>
) : null
}
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.900' : 'text.primary',
}}
componentsProps={{
badge: {
className: blink ? 'animate-pulse' : '',
},
}}
{...props}
>
{children}
</Badge>
</Badge>
);
}
return (
<Badge
color={badgeColor}
variant={badgeVariant}
badgeContent={
showCheckIcon ? (
<CheckIcon
sx={{
color: (theme) =>
theme.palette.mode === 'dark' ? 'grey.200' : 'grey.100',
}}
className="h-2 w-2 stroke-2"
/>
) : null
}
componentsProps={{
badge: {
className: blink ? 'animate-pulse' : '',
},
}}
{...props}
>
{children}
</Badge>
);
}
export interface ProjectHealthCardProps extends BoxProps {
/**
* Label of the card icon.
@@ -171,7 +71,11 @@ export default function ProjectHealthCard({
...props
}: ProjectHealthCardProps) {
const badgeColor = serviceStateToBadgeColor.get(state);
const badgeVariant = state === ServiceState.Running ? 'standard' : 'dot';
const unknownState = state === undefined;
let badgeVariant: 'dot' | 'standard' = 'dot';
if (state === ServiceState.Running || unknownState) {
badgeVariant = 'standard';
}
const showCheckIcon = state === ServiceState.Running;
const shouldBlink = state === ServiceState.Updating;
@@ -199,11 +103,12 @@ export default function ProjectHealthCard({
{...props}
>
<div className="grid grid-flow-col items-center justify-center">
<HealthBadge
<ProjectHealthBadge
badgeColor={!isLoading ? badgeColor : undefined}
badgeVariant={badgeVariant}
showCheckIcon={showCheckIcon}
showExclamation={isVersionMismatch}
unknownState={unknownState}
blink={shouldBlink}
>
{iconIsComponent
@@ -217,7 +122,7 @@ export default function ProjectHealthCard({
{...slotProps.imgIcon}
/>
)}
</HealthBadge>
</ProjectHealthBadge>
</div>
</Box>
</Tooltip>

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,19 +35,19 @@ export const serviceStateToThemeColor = new Map<ServiceState, string>([
[ServiceState.UpdateError, 'error.main'],
[ServiceState.Updating, 'warning.dark'],
[ServiceState.None, 'error.main'],
[undefined, 'error.main'],
[undefined, 'grey.500'],
]);
export const serviceStateToBadgeColor = new Map<
ServiceState,
'success' | 'error' | 'warning'
'success' | 'error' | 'warning' | 'secondary'
>([
[ServiceState.Running, 'success'],
[ServiceState.Error, 'error'],
[ServiceState.UpdateError, 'error'],
[ServiceState.Updating, 'warning'],
[ServiceState.None, 'error'],
[undefined, 'error'],
[undefined, 'secondary'], // secondary is used for unknown states
]);
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,11 @@
# @nhost-examples/react-apollo
## 0.8.8
### Patch Changes
- e3f0732: fix: add verify email button instead of doing an auto-redirect
## 0.8.7
### Patch Changes

View File

@@ -31,7 +31,8 @@ test('should be able to change email', async ({ page, browser }) => {
page: mailhogPage,
email: newEmail,
context: mailhogPage.context(),
linkText: /change email/i
linkText: /change email/i,
requestType: 'email-confirm-change'
})
await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()

View File

@@ -158,8 +158,8 @@ export async function resetPassword({
.click()
const authenticatedPage = await authenticatedPagePromise
await authenticatedPage.getByRole('button', { name: /Verify/i }).click()
await authenticatedPage.waitForLoadState()
return authenticatedPage
}
@@ -177,25 +177,31 @@ export async function verifyEmail({
email,
page,
context,
linkText = /verify email/i
linkText = /verify email/i,
requestType
}: {
email: string
page: Page
context: BrowserContext
linkText?: string | RegExp
requestType?: 'email-confirm-change' | 'email-verify' | 'password-reset' | 'signin-passwordless'
}) {
await page.goto(mailhogURL)
await page.locator('.messages > .msglist-message', { hasText: email }).nth(0).click()
// Based on: https://playwright.dev/docs/pages#handling-new-pages
const authenticatedPagePromise = context.waitForEvent('page')
const verifyEmailPagePromise = context.waitForEvent('page')
await page.frameLocator('#preview-html').getByRole('link', { name: linkText }).click()
const verifyEmailPage = await verifyEmailPagePromise
await verifyEmailPage.waitForLoadState()
const authenticatedPage = await authenticatedPagePromise
await authenticatedPage.waitForLoadState()
if (requestType === 'email-confirm-change') {
return verifyEmailPage
}
return authenticatedPage
await verifyEmailPage.getByRole('button', { name: /Verify/i }).click()
await verifyEmailPage.waitForLoadState()
return verifyEmailPage
}
/**

View File

@@ -1,7 +1,7 @@
[global]
[hasura]
version = 'v2.33.4-ce'
version = 'v2.38.0-ce'
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
@@ -29,7 +29,7 @@ httpPoolSize = 100
version = 18
[auth]
version = '0.28.0-beta002'
version = '0.32.0'
[auth.elevatedPrivileges]
mode = 'required'
@@ -154,7 +154,7 @@ enabled = true
issuer = 'nhost'
[postgres]
version = '14.6-20240129-1'
version = '14.11-20240515-1'
[provider]

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/react-apollo",
"version": "0.8.7",
"version": "0.8.8",
"private": true,
"dependencies": {
"@apollo/client": "^3.9.9",

View File

@@ -1,32 +1,39 @@
import { useSearchParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { Button, Card, Container, Stack, Text } from '@mantine/core'
import { showNotification } from '@mantine/notifications'
import { useNhostClient } from '@nhost/react'
import { Container } from '@mantine/core'
import { FaEnvelope } from 'react-icons/fa'
import { useSearchParams } from 'react-router-dom'
const VerifyPage: React.FC = () => {
const nhost = useNhostClient()
const [loading, setLoading] = useState(false)
const [searchParams] = useSearchParams()
useEffect(() => {
const redirectToVerificationLink = () => {
const ticket = searchParams.get('ticket')
const redirectTo = searchParams.get('redirectTo')
const type = searchParams.get('type')
const redirectTo = searchParams.get('redirectTo')
if (ticket && redirectTo && type) {
if (ticket && type && redirectTo) {
window.location.href = `${nhost.auth.url}/verify?ticket=${ticket}&type=${type}&redirectTo=${redirectTo}`
} else {
showNotification({
color: 'red',
title: 'Error',
message: 'An error occured while verifying your account'
})
}
setLoading(false)
}, [searchParams, nhost?.auth?.url])
if (loading) {
return null
}
return (
<Container>
<span>Failed to authenticate with magick link</span>
<Card shadow="sm" p="lg" radius="md" withBorder>
<Stack align="center">
<Text>Please verify your account by clicking the link below.</Text>
<Button leftIcon={<FaEnvelope size={14} />} onClick={redirectToVerificationLink}>
Verify
</Button>
</Stack>
</Card>
</Container>
)
}

View File

@@ -91,7 +91,7 @@
"resolutions": {
"graphql": "16.8.1"
},
"packageManager": "pnpm@8.6.2",
"packageManager": "pnpm@8.10.5",
"engines": {
"node": ">=18 <19",
"pnpm": ">=8.0.0"

161
pnpm-lock.yaml generated
View File

@@ -248,6 +248,9 @@ importers:
'@tanstack/react-virtual':
specifier: ^3.2.0
version: 3.5.0(react-dom@18.2.0)(react@18.2.0)
'@uidotdev/usehooks':
specifier: ^2.4.1
version: 2.4.1(react-dom@18.2.0)(react@18.2.0)
'@uiw/codemirror-theme-bbedit':
specifier: ^4.22.2
version: 4.22.2(@codemirror/language@6.10.2)
@@ -1070,16 +1073,16 @@ importers:
version: 3.59.2
svelte-check:
specifier: ^3.6.8
version: 3.6.8(postcss@8.4.38)(svelte@3.59.2)
version: 3.6.8(@babel/core@7.24.7)(postcss@8.4.38)(svelte@3.59.2)
tailwindcss:
specifier: ^3.4.3
version: 3.4.3
version: 3.4.3(ts-node@10.9.2)
typescript:
specifier: ^5.4.3
version: 5.4.3
vite:
specifier: ^5.2.7
version: 5.2.7(@types/node@20.14.8)
version: 5.2.7(@types/node@16.18.101)(sass@1.32.0)
vitest:
specifier: ^0.25.8
version: 0.25.8
@@ -1976,10 +1979,10 @@ importers:
version: 3.10.6(@types/react@18.3.3)(react@18.2.0)
'@nhost/react':
specifier: ^3.5.2
version: link:../../../packages/react
version: 3.5.2(@types/react@18.3.3)(react@18.2.0)
'@nhost/react-apollo':
specifier: ^12.0.2
version: link:../../../integrations/react-apollo
version: 12.0.2(@apollo/client@3.10.6)(@nhost/react@3.5.2)(react@18.2.0)
'@react-native-async-storage/async-storage':
specifier: ^1.23.1
version: 1.23.1(react-native@0.73.7)
@@ -2373,7 +2376,7 @@ packages:
resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==}
hasBin: true
peerDependencies:
graphql: '>=16.8.1'
graphql: 16.8.1
dependencies:
'@babel/core': 7.24.6
'@babel/generator': 7.24.7
@@ -8574,7 +8577,7 @@ packages:
/@graphql-tools/utils@8.13.1(graphql@16.8.1):
resolution: {integrity: sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==}
peerDependencies:
graphql: '>=16.8.1'
graphql: 16.8.1
dependencies:
graphql: 16.8.1
tslib: 2.6.2
@@ -8613,7 +8616,7 @@ packages:
/@graphql-typed-document-node/core@3.2.0(graphql@16.8.1):
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
peerDependencies:
graphql: '>=16.8.1'
graphql: 16.8.1
dependencies:
graphql: 16.8.1
@@ -10281,6 +10284,18 @@ packages:
requiresBuild: true
optional: true
/@nhost/apollo@7.1.2(@apollo/client@3.10.6):
resolution: {integrity: sha512-sBd4Go6HkbwdQSP/teP7py8SveMrGZQtK3SyF9QrC59m5/tmkTJbPJJdJuWAmi3Lr8/xHNFaFxAtMWjfGx/VyA==}
peerDependencies:
'@apollo/client': ^3.7.10
'@nhost/nhost-js': 3.1.5
dependencies:
'@apollo/client': 3.10.6(@types/react@18.3.3)(react@18.2.0)
graphql: 16.8.1
graphql-ws: 5.16.0(graphql@16.8.1)
jwt-decode: 4.0.0
dev: false
/@nhost/graphql-js@0.3.0(graphql@16.8.1):
resolution: {integrity: sha512-CVYq6wx0VbaYdpUBmfNO/6mZatHB5+YBCqFjWyxhpN1nzHCHEO6rgdL7j0qk31OFE6XAX0z7AQZSXg1Pn63GUw==}
peerDependencies:
@@ -10293,7 +10308,6 @@ packages:
jwt-decode: 4.0.0
transitivePeerDependencies:
- encoding
dev: true
/@nhost/hasura-auth-js@2.5.2:
resolution: {integrity: sha512-3O4fIJ8xbdCdKGR/1o5jMczxrLLQ2g6BNp6J9m83COVqg9ka5IXoFuM6pgbX5W7WPe9nIQntvHsfeDynXS+/fg==}
@@ -10305,7 +10319,6 @@ packages:
xstate: 4.38.3
transitivePeerDependencies:
- encoding
dev: true
/@nhost/hasura-storage-js@2.5.1:
resolution: {integrity: sha512-I3rOSa095lcR9BUmNw7dOoXLPWL39WOcrb0paUBFX4h3ltR92ILEHTZ38hN6bZSv157ZdqkIFNL/M2G45SSf7g==}
@@ -10316,7 +10329,6 @@ packages:
xstate: 4.38.3
transitivePeerDependencies:
- encoding
dev: true
/@nhost/nhost-js@3.1.5(graphql@16.8.1):
resolution: {integrity: sha512-SgDGQ0APiRPc6RB2Cl1EcvQUsmWFDx32JmgqYBgLKKV9+PBDKc2GJ4GHECbxQi3RoIMXeJ777XJTEK7mGgNL+A==}
@@ -10330,7 +10342,41 @@ packages:
isomorphic-unfetch: 3.1.0
transitivePeerDependencies:
- encoding
dev: true
/@nhost/react-apollo@12.0.2(@apollo/client@3.10.6)(@nhost/react@3.5.2)(react@18.2.0):
resolution: {integrity: sha512-Ll5RKH5g9aFTBBnTjlQCWmrHeyjhnzy1bx0TAG1FsXjh3YbsLPqelmVtAks2t7m+Uw8XWZ37D+cL/Hum/KxG/w==}
peerDependencies:
'@apollo/client': ^3.7.10
'@nhost/react': 3.5.2
graphql: '>=16.8.1'
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
dependencies:
'@apollo/client': 3.10.6(@types/react@18.3.3)(react@18.2.0)
'@nhost/apollo': 7.1.2(@apollo/client@3.10.6)
'@nhost/react': 3.5.2(@types/react@18.3.3)(react@18.2.0)
react: 18.2.0
transitivePeerDependencies:
- '@nhost/nhost-js'
dev: false
/@nhost/react@3.5.2(@types/react@18.3.3)(react@18.2.0):
resolution: {integrity: sha512-i/qE9k3ccLyMum4JM1aSDc+qPZPW/o4Rgv7mn6Dc2qWEuDEYrOffqMJ/52nnT8k2O8GeHFpu2BZY81SCuiao+w==}
peerDependencies:
react: ^17.0.0 || ^18.1.0
dependencies:
'@nhost/nhost-js': 3.1.5(graphql@16.8.1)
'@xstate/react': 3.2.2(@types/react@18.3.3)(react@18.2.0)(xstate@4.38.3)
jwt-decode: 4.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
xstate: 4.38.3
transitivePeerDependencies:
- '@types/react'
- '@xstate/fsm'
- encoding
- graphql
dev: false
/@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1:
resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
@@ -13266,7 +13312,7 @@ packages:
svelte: 3.59.2
tiny-glob: 0.2.9
undici: 5.28.4
vite: 5.2.7(@types/node@20.14.8)
vite: 5.2.7(@types/node@16.18.101)(sass@1.32.0)
transitivePeerDependencies:
- supports-color
dev: true
@@ -13282,7 +13328,7 @@ packages:
'@sveltejs/vite-plugin-svelte': 2.5.3(svelte@3.59.2)(vite@5.2.7)
debug: 4.3.5
svelte: 3.59.2
vite: 5.2.7(@types/node@20.14.8)
vite: 5.2.7(@types/node@16.18.101)(sass@1.32.0)
transitivePeerDependencies:
- supports-color
dev: true
@@ -13301,7 +13347,7 @@ packages:
magic-string: 0.30.8
svelte: 3.59.2
svelte-hmr: 0.15.3(svelte@3.59.2)
vite: 5.2.7(@types/node@20.14.8)
vite: 5.2.7(@types/node@16.18.101)(sass@1.32.0)
vitefu: 0.2.5(vite@5.2.7)
transitivePeerDependencies:
- supports-color
@@ -14655,6 +14701,17 @@ packages:
'@typescript-eslint/types': 6.21.0
eslint-visitor-keys: 3.4.3
/@uidotdev/usehooks@2.4.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==}
engines: {node: '>=16'}
peerDependencies:
react: '>=18.0.0'
react-dom: '>=18.0.0'
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@uiw/codemirror-extensions-basic-setup@4.22.1(@codemirror/language@6.10.2):
resolution: {integrity: sha512-Iz8eFaZBNrwjaAADszOxOv2byDMn4rqob/luuSPAzJjTrSn5KawRXcoNLoWGPGNO6Mils6bIly/g2LaU34otNw==}
peerDependencies:
@@ -14902,7 +14959,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.24.6(@babel/core@7.24.7)
magic-string: 0.27.0
react-refresh: 0.14.2
vite: 5.2.7(@types/node@16.18.98)
vite: 5.2.7(@types/node@18.19.34)
transitivePeerDependencies:
- supports-color
dev: true
@@ -21761,7 +21818,7 @@ packages:
resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==}
engines: {node: '>=10'}
peerDependencies:
graphql: '>=16.8.1'
graphql: 16.8.1
dependencies:
graphql: 16.8.1
tslib: 2.6.2
@@ -23259,7 +23316,7 @@ packages:
/isomorphic-unfetch@3.1.0:
resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
dependencies:
node-fetch: 2.7.0
node-fetch: 2.7.0(encoding@0.1.13)
unfetch: 4.2.0
transitivePeerDependencies:
- encoding
@@ -26725,17 +26782,6 @@ packages:
dependencies:
whatwg-url: 5.0.0
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
/node-fetch@2.7.0(encoding@0.1.13):
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -27740,23 +27786,6 @@ packages:
yaml: 1.10.2
dev: true
/postcss-load-config@4.0.2(postcss@8.4.38):
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
peerDependencies:
postcss: '>=8.4.31'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
dependencies:
lilconfig: 3.1.1
postcss: 8.4.38
yaml: 2.4.3
dev: true
/postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2):
resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==}
engines: {node: '>= 14'}
@@ -30918,7 +30947,7 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/svelte-check@3.6.8(postcss@8.4.38)(svelte@3.59.2):
/svelte-check@3.6.8(@babel/core@7.24.7)(postcss@8.4.38)(svelte@3.59.2):
resolution: {integrity: sha512-rhXU7YCDtL+lq2gCqfJDXKTxJfSsCgcd08d7VWBFxTw6IWIbMWSaASbAOD3N0VV9TYSSLUqEBiratLd8WxAJJA==}
hasBin: true
peerDependencies:
@@ -30931,7 +30960,7 @@ packages:
picocolors: 1.0.1
sade: 1.8.1
svelte: 3.59.2
svelte-preprocess: 5.1.3(postcss@8.4.38)(svelte@3.59.2)(typescript@5.4.3)
svelte-preprocess: 5.1.3(@babel/core@7.24.7)(postcss@8.4.38)(svelte@3.59.2)(typescript@5.4.3)
typescript: 5.4.3
transitivePeerDependencies:
- '@babel/core'
@@ -30971,7 +31000,7 @@ packages:
svelte: 3.59.2
dev: true
/svelte-preprocess@5.1.3(postcss@8.4.38)(svelte@3.59.2)(typescript@5.4.3):
/svelte-preprocess@5.1.3(@babel/core@7.24.7)(postcss@8.4.38)(svelte@3.59.2)(typescript@5.4.3):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
requiresBuild: true
@@ -31009,6 +31038,7 @@ packages:
typescript:
optional: true
dependencies:
'@babel/core': 7.24.7
'@types/pug': 2.0.10
detect-indent: 6.1.0
magic-string: 0.30.8
@@ -31103,37 +31133,6 @@ packages:
- ts-node
dev: false
/tailwindcss@3.4.3:
resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.2
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.0
lilconfig: 2.1.0
micromatch: 4.0.7
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.0.1
postcss: 8.4.38
postcss-import: 15.1.0(postcss@8.4.38)
postcss-js: 4.0.1(postcss@8.4.38)
postcss-load-config: 4.0.2(postcss@8.4.38)
postcss-nested: 6.0.1(postcss@8.4.38)
postcss-selector-parser: 6.1.0
resolve: 1.22.8
sucrase: 3.35.0
transitivePeerDependencies:
- ts-node
dev: true
/tailwindcss@3.4.3(ts-node@10.9.2):
resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==}
engines: {node: '>=14.0.0'}
@@ -33157,7 +33156,7 @@ packages:
vite:
optional: true
dependencies:
vite: 5.2.7(@types/node@20.14.8)
vite: 5.2.7(@types/node@16.18.101)(sass@1.32.0)
dev: true
/vitest@0.25.8: