Compare commits

...

76 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
f0da84bbec Merge pull request #2427 from nhost/changeset-release/main
chore: update versions
2023-12-22 16:48:43 +01:00
github-actions[bot]
5efa43aa2e chore: update versions 2023-12-22 15:47:34 +00:00
Hassan Ben Jobrane
2497194dcc Merge pull request #2415 from nhost/feat/project-g
feat: project g
2023-12-22 16:45:30 +01:00
Hassan Ben Jobrane
5733162ed6 chore: add changeset 2023-12-22 16:23:53 +01:00
Hassan Ben Jobrane
ab106c9492 chore: run pnpm install 2023-12-22 16:22:50 +01:00
Hassan Ben Jobrane
4d2aac807c chore: refactor dev-assistant and optimize rendering of messages 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
a659760724 chore: update content of tooltips 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
13086bcae3 feat: show assistantId in the assistants list 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
86459468be fix: remove dataSources from assistants form 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
34cec77ceb feat: add copy code block button 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
abfb42651a feat: add confirmation dialog when disabling graphite 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
ec584181cc feat: show cost approximation for ai resources 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
70b31358bc feat: add remark-gfm plugin to Markdown rendering 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
521f418f8c chore: add pro upgrade banners 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
8851416e7a fix: prevent disable ai service from firing on first load 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
f98f5a4bca feat: add labels and tooltips to the ai settings 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
650a605b61 feat: update settings page 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
422e1bbeae fix: make sure to send prevMessageID 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
367e86abd2 fix: use empty prevMessageID when starting a new thread 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
7e172d6352 fix: use item name to view and delete items in the lists 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
e786a6fa84 fix: adjust markdown rendering in dark mode 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
f899f4000d feat: add Tailwind Typography plugin and GitHub Dark theme CSS file 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
ecd27f34d6 fix: typo in sessionID parameter and reformat code in getAssistants query 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
9f488d2739 fix: pull graphite versions from graphql api 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
fac066c0cd fix: make sure version field is updated properly 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
fdc56e9611 feat: add all graphite settings fields 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
7b11f343ac fix: exclude graphite gql files from code generation 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
04d39bef90 fix: code line wrapping + show banner when project is on the free plan 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
83e21f879f feat: add settings related to project-g 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
8e26cdb5ed chore: fix test to account for new nav bar item 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
4dc1a5ded3 chore: remove console.log 2023-12-22 16:22:12 +01:00
Hassan Ben Jobrane
b3f6c732dd feat: add feature related to project-g 2023-12-22 16:22:11 +01:00
Hassan Ben Jobrane
a63342d0bd fix: add name field to the GraphQL query 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
4913ff7a8b chore: remove unused import 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
99cbbbcbf9 chore: remove console.log 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
3a11b6a8fa feat(project-g): make inputs resizable and fix the update mutation 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
be4b26c65d feat: add basic list and edit func 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
33df3c842d wip: feat: add layout and basic crud 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
a5bba46b59 fix: UI tweaks 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
1358a41dc4 feat: add ui components for project g 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
2b7cf59159 feat: add layout for project g 2023-12-22 16:19:09 +01:00
Hassan Ben Jobrane
083c65b775 Merge pull request #2426 from nhost/chore/fix-eslint
chore: update eslint
2023-12-22 15:51:25 +01:00
Hassan Ben Jobrane
1c940469fb chore: update eslint 2023-12-22 15:20:07 +01:00
github-actions[bot]
e2bf1118f9 chore: update versions (#2424)
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.1.0

### Minor Changes

-   e2b79b5ec: chore: remove sharp from deps

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-12-22 14:51:45 +01:00
Hassan Ben Jobrane
9a1ad43370 Merge pull request #2423 from nhost/chore/new-release
chore: add changeset
2023-12-22 14:48:49 +01:00
Hassan Ben Jobrane
e2b79b5ece chore: add changeset 2023-12-22 14:48:14 +01:00
Hassan Ben Jobrane
c47d47ac9c Merge pull request #2422 from nhost/chore/remove-sharp-package
chore(dashboard): remove sharp package from dependencies
2023-12-22 14:22:52 +01:00
Hassan Ben Jobrane
926590acb5 chore(dashboard): remove sharp package from dependencies 2023-12-22 14:09:49 +01:00
Hassan Ben Jobrane
90e8843314 Merge pull request #2421 from nhost/changeset-release/main
chore: update versions
2023-12-22 11:51:01 +01:00
github-actions[bot]
aa5b360932 chore: update versions 2023-12-22 10:30:28 +00:00
Hassan Ben Jobrane
daa4b8b2ad Merge pull request #2400 from nhost/changeset-release/main
chore: update versions
2023-12-22 11:28:17 +01:00
Seth Deegan
a1c5c97a59 chore (examples/docker-compose): update README.md to explain why hasura-console is needed (#2395) 2023-12-11 20:14:59 +01:00
Alex Nguyen
b338793d6d Update hasura-auth-client.ts (#2408) 2023-12-11 13:44:00 +01:00
Hassan Ben Jobrane
b1fb4b2400 chore: run pnpm install 2023-12-07 19:49:14 +01:00
github-actions[bot]
f75e023672 chore: update versions 2023-12-05 15:18:53 +00:00
Hassan Ben Jobrane
8e78c1ff00 Merge pull request #2406 from nhost/fix/ci/revert
chore(ci): revert ci changes to use `pull_request`
2023-12-05 16:16:39 +01:00
Hassan Ben Jobrane
9cbb0b2986 chore(ci): revert ci changes to use pull_request 2023-12-05 14:09:53 +01:00
Hassan Ben Jobrane
363a3b92e5 Merge pull request #2405 from nhost/fix/ci/checkout-ref
fix(ci): add ref to all checkout steps
2023-12-05 12:58:47 +01:00
Hassan Ben Jobrane
6a078fc972 fix(ci): add ref to all checkout steps 2023-12-05 12:51:47 +01:00
Hassan Ben Jobrane
1091e9674a Merge pull request #2404 from nhost/fix/ci-checkout-step
chore(ci): add ref to checkout step
2023-12-05 12:26:57 +01:00
Hassan Ben Jobrane
9738108d58 chore(ci): add ref to checkout step 2023-12-05 12:13:53 +01:00
Hassan Ben Jobrane
65951e1d1d Merge pull request #2403 from nhost/ci_target
chore(ci): change to pull_request_target to run workflows "locally"
2023-12-05 11:55:30 +01:00
David Barroso
b4af994a58 chore(ci): change pull_request to pull_request_target to run workflows locally 2023-12-05 11:39:58 +01:00
Hassan Ben Jobrane
c6347e10bc Merge pull request #2402 from nhost/fix/ci/pin-install-nhost-dep
fix(ci): pin `@nhost/nhost-js` dep version in sveltekit quickstart
2023-12-04 17:30:10 +01:00
Hassan Ben Jobrane
278a641bc1 fix(ci): pin @nhost/nhost-js dep version in sveltekit quickstart 2023-12-04 16:18:02 +01:00
Hassan Ben Jobrane
3320ddd8c8 Merge pull request #2393 from nhost/chore/sdk/remove-backendUrl
chore: remove support for using `backendUrl`
2023-12-04 15:05:52 +01:00
Hassan Ben Jobrane
bc9eff6e41 chore: update the changeset to reflect a major version increment 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
258c608882 Revert "chore: hardcode staging auth URL for testing"
This reverts commit d8c0bb5ea4e073a7131df3726728845b2bc5e1a1.
2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
ae84f269d4 chore: hardcode staging auth URL for testing 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
0327250b19 Revert "chore: test different subdomain"
This reverts commit 9dfd9399a0a0b1ec931e02304dbe62183b2cb500.
2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
7f56eabd24 chore: test different subdomain 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
be110df83a fix: refactor urlFromSubdomain and fix unit tests 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
361e648daf chore: add changeset 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
8a72e20e3d chore: refactor generateAppServiceUrl function and remove unused code 2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
125ec390ca chore: add storage service URL to Nhost client
configuration
2023-12-04 14:38:56 +01:00
Hassan Ben Jobrane
7cc788a373 refactor: remove backendUrl from Nhost client initialization 2023-12-04 14:38:56 +01:00
135 changed files with 13437 additions and 513 deletions

View File

@@ -1,5 +0,0 @@
---
'@nhost/docs': patch
---
added functions to custom domains documentation

View File

@@ -1,5 +1,36 @@
# @nhost/dashboard
## 1.2.0
### Minor Changes
- 5733162ed: feat: add settings and ui for graphite
## 1.1.0
### Minor Changes
- e2b79b5ec: chore: remove sharp from deps
## 1.0.1
### Patch Changes
- @nhost/react-apollo@7.0.1
- @nhost/nextjs@2.0.1
## 1.0.0
### Major Changes
- bc9eff6e4: chore: remove support for using backendUrl when instantiating the Nhost client
### Patch Changes
- Updated dependencies [bc9eff6e4]
- @nhost/nextjs@2.0.0
- @nhost/react-apollo@7.0.0
## 0.21.1
### Patch Changes

View File

@@ -30,7 +30,7 @@ test('should show a sidebar with menu items', async () => {
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
await expect(navLocator).toBeVisible();
await expect(navLocator.getByRole('list').getByRole('listitem')).toHaveCount(
12,
13,
);
await expect(
navLocator.getByRole('link', { name: /overview/i }),

View File

@@ -0,0 +1,14 @@
schema:
- https://local.graphql.nhost.run/v1:
headers:
x-hasura-admin-secret: nhost-admin-secret
generates:
src/utils/__generated__/graphite.graphql.ts:
documents:
- 'src/gql/graphite/**/*.gql'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
config:
withRefetchFn: true

View File

@@ -7,6 +7,7 @@ generates:
documents:
- 'src/**/*.graphql'
- 'src/**/*.gql'
- '!src/gql/graphite/**/*.gql'
plugins:
- 'typescript'
- 'typescript-operations'

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.21.1",
"version": "1.2.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -11,6 +11,7 @@
"lint": "next lint --max-warnings 0",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"codegen-graphite": "graphql-codegen --config graphite.graphql.config.yaml --errors-only",
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook",
@@ -64,6 +65,7 @@
"node-pg-format": "^1.3.5",
"pluralize": "^8.0.0",
"react": "18.2.0",
"react-children-utilities": "^2.9.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.0",
"react-hook-form": "^7.42.1",
@@ -71,11 +73,14 @@
"react-intersection-observer": "^9.5.2",
"react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0",
"react-markdown": "^9.0.1",
"react-merge-refs": "^1.1.0",
"react-resizable-layout": "^0.7.2",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"sharp": "^0.32.0",
"recoil": "^0.7.7",
"recoil-persist": "^5.1.0",
"rehype-highlight": "^7.0.0",
"remark-gfm": "^4.0.0",
"shell-quote": "^1.8.1",
"slugify": "^1.6.5",
"stripe": "^10.17.0",
@@ -103,6 +108,7 @@
"@storybook/manager-webpack5": "^6.5.14",
"@storybook/react": "^6.5.14",
"@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.10",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
@@ -119,8 +125,8 @@
"@types/shell-quote": "^1.7.1",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-v8": "^0.32.0",
"autoprefixer": "^10.4.13",

View File

@@ -9,10 +9,11 @@ import { ChangePlanModal } from '@/features/projects/common/components/ChangePla
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import Image from 'next/image';
import { type ReactNode } from 'react';
interface UpgradeToProBannerProps {
title: string;
description: string;
description: string | ReactNode;
}
export default function UpgradeToProBanner({
@@ -25,7 +26,7 @@ export default function UpgradeToProBanner({
return (
<Box
sx={{ backgroundColor: 'primary.light' }}
className="flex flex-col p-4 space-y-4 rounded-md lg:flex-row lg:items-center lg:space-y-0"
className="flex flex-col justify-between space-y-4 rounded-md p-4 lg:flex-row lg:items-center lg:space-y-0"
>
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2">
@@ -39,7 +40,11 @@ export default function UpgradeToProBanner({
</div>
</div>
<Text variant="h3">{title}</Text>
<Text>{description}</Text>
{typeof description === 'string' ? (
<Text>{description}</Text>
) : (
description
)}
</div>
<div className="flex flex-col space-y-2 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-2">
@@ -76,25 +81,23 @@ export default function UpgradeToProBanner({
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium text-center"
className="text-center font-medium"
sx={{
color: 'text.secondary',
}}
>
See all features
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</div>
</div>
<div className="max-w-xs mx-auto">
<Image
src="/illustration-unbox.png"
width={400}
height={260}
objectFit="contain"
/>
</div>
<Image
src="/illustration-unbox.png"
width={300}
height={140}
objectFit="contain"
/>
</Box>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { ContactUs } from '@/components/common/ContactUs';
import { useDialog } from '@/components/common/DialogProvider';
import { NavLink } from '@/components/common/NavLink';
import { AccountMenu } from '@/components/layout/AccountMenu';
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
@@ -6,14 +7,19 @@ import { LocalAccountMenu } from '@/components/layout/LocalAccountMenu';
import { MobileNav } from '@/components/layout/MobileNav';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { ApplicationStatus } from '@/types/application';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useRouter } from 'next/router';
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface HeaderProps
@@ -23,9 +29,14 @@ export interface HeaderProps
export default function Header({ className, ...props }: HeaderProps) {
const router = useRouter();
const isPlatform = useIsPlatform();
const { openDrawer } = useDialog();
const { currentProject, refetch: refetchProject } =
useCurrentWorkspaceAndProject();
const isProjectUpdating =
currentProject?.appStates[0]?.stateId === ApplicationStatus.Updating;
@@ -44,6 +55,23 @@ export default function Header({ className, ...props }: HeaderProps) {
};
}, [isProjectUpdating, refetchProject]);
const openDevAssistant = () => {
// The dev assistant can be only answer questions related to a particular project
if (!currentProject) {
toast.error('You need to be inside a project to open the Assistant', {
style: getToastStyleProps().style,
...getToastStyleProps().error,
});
return;
}
openDrawer({
title: <GraphiteIcon />,
component: <DevAssistant />,
});
};
return (
<Box
component="header"
@@ -54,7 +82,7 @@ export default function Header({ className, ...props }: HeaderProps) {
sx={{ backgroundColor: 'background.paper' }}
{...props}
>
<div className="grid grid-flow-col items-center gap-3 ">
<div className="grid grid-flow-col items-center gap-3">
<NavLink href="/" className="w-12">
<Logo className="mx-auto cursor-pointer" />
</NavLink>
@@ -69,6 +97,10 @@ export default function Header({ className, ...props }: HeaderProps) {
</div>
<div className="hidden grid-flow-col items-center gap-2 sm:grid">
<Button className="rounded-full" onClick={openDevAssistant}>
<GraphiteIcon />
</Button>
{isPlatform && (
<Dropdown.Root>
<Dropdown.Trigger

View File

@@ -208,6 +208,9 @@ export default function SettingsSidebar({
>
Custom Domains
</SettingsNavLink>
<SettingsNavLink href="/ai" exact={false} onClick={handleSelect}>
AI
</SettingsNavLink>
</List>
</nav>
</Box>

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1,32 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function ArrowElbowRightUp(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8 6L11 3L14 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M2 12H11V3"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</SvgIcon>
);
}
ArrowElbowRightUp.displayName = 'NhostArrowElbowRightUp';
export default ArrowElbowRightUp;

View File

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

View File

@@ -0,0 +1,33 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function EmbeddingsIcon(props: IconProps) {
return (
<SvgIcon
width="17"
height="17"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 17 17"
fill="none"
aria-label="Embeddings Icon"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.178057 4.04687L4.04687 0.178057C4.28428 -0.0593522 4.6692 -0.0593522 4.90661 0.178057L8.77542 4.04687C9.01283 4.28428 9.01283 4.6692 8.77542 4.90661C8.53801 5.14402 8.15309 5.14402 7.91568 4.90661L5.08466 2.07559L5.08466 12.7664H3.86881L3.86881 2.07559L1.03779 4.90661C0.800384 5.14402 0.415467 5.14402 0.178057 4.90661C-0.0593524 4.6692 -0.0593524 4.28428 0.178057 4.04687Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.9531 8.22458L16.8219 12.0934C17.0594 12.3308 17.0594 12.7157 16.8219 12.9531L12.9531 16.8219C12.7157 17.0594 12.3308 17.0594 12.0934 16.8219C11.856 16.5845 11.856 16.1996 12.0934 15.9622L14.9244 13.1312H4.23357V11.9153H14.9244L12.0934 9.08432C11.856 8.84691 11.856 8.46199 12.0934 8.22458C12.3308 7.98717 12.7157 7.98717 12.9531 8.22458Z"
fill="currentColor"
/>
</SvgIcon>
);
}
EmbeddingsIcon.displayName = 'NhostEmbeddingsIcon';
export default EmbeddingsIcon;

View File

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

View File

@@ -0,0 +1,36 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function GraphiteIcon(props: IconProps) {
return (
<SvgIcon
width="22"
height="25"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 22 25"
aria-label="Graphite"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.39873 13.0137C12.2825 13.0137 14.6203 10.7138 14.6203 7.87671C14.6203 5.03963 12.2825 2.73973 9.39873 2.73973C6.51497 2.73973 4.17722 5.03963 4.17722 7.87671C4.17722 10.7138 6.51497 13.0137 9.39873 13.0137ZM9.39873 15.7534C13.8205 15.7534 17.4051 12.2269 17.4051 7.87671C17.4051 3.52652 13.8205 0 9.39873 0C4.97696 0 1.39241 3.52652 1.39241 7.87671C1.39241 12.2269 4.97696 15.7534 9.39873 15.7534Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2.78481 15.7534C2.78481 19.3471 5.74597 22.2603 9.39873 22.2603C13.0515 22.2603 16.0127 19.3471 16.0127 15.7534H18.7975C18.7975 20.8602 14.5895 25 9.39873 25C4.20796 25 0 20.8602 0 15.7534H2.78481Z"
fill="currentColor"
/>
<path
d="M7.37975 1.36986C7.37975 0.613309 8.00315 0 8.77215 0H20.6076C21.3766 0 22 0.613309 22 1.36986C22 2.12642 21.3766 2.73973 20.6076 2.73973H8.77215C8.00315 2.73973 7.37975 2.12642 7.37975 1.36986Z"
fill="currentColor"
/>
</SvgIcon>
);
}
GraphiteIcon.displayName = 'NhostGraphiteIcon';
export default GraphiteIcon;

View File

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

View File

@@ -0,0 +1,345 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { removeTypename, type DeepRequired } from '@/utils/helpers';
import {
useInsertAssistantMutation,
useUpdateAssistantMutation,
} from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
name: Yup.string().required('The name is required.'),
description: Yup.string(),
instructions: Yup.string().required('The instructions are required'),
model: Yup.string().required('The model is required'),
graphql: Yup.array().of(
Yup.object().shape({
name: Yup.string().required(),
description: Yup.string().required(),
query: Yup.string().required(),
arguments: Yup.array().of(
Yup.object().shape({
name: Yup.string().required(),
description: Yup.string().required(),
type: Yup.string().required(),
required: Yup.bool().required(),
}),
),
}),
),
webhooks: Yup.array().of(
Yup.object().shape({
name: Yup.string().required(),
description: Yup.string().required(),
URL: Yup.string().required(),
arguments: Yup.array().of(
Yup.object().shape({
name: Yup.string().required(),
description: Yup.string().required(),
type: Yup.string().required(),
required: Yup.bool().required(),
}),
),
}),
),
});
export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
export interface AssistantFormProps extends DialogFormProps {
/**
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
*/
assistantId?: string;
/**
* if there is initialData then it's an update operation
*/
initialData?: AssistantFormValues;
/**
* Function to be called when the operation is cancelled.
*/
onCancel?: VoidFunction;
/**
* Function to be called when the submit is successful.
*/
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
}
export default function AssistantForm({
assistantId,
initialData,
onSubmit,
onCancel,
location,
}: AssistantFormProps) {
const { onDirtyStateChange } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [insertAssistantMutation] = useInsertAssistantMutation({
client,
});
const [updateAssistantMutation] = useUpdateAssistantMutation({ client });
const form = useForm<AssistantFormValues>({
defaultValues: initialData,
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
const {
register,
formState: { errors, isSubmitting, dirtyFields },
} = form;
const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const createOrUpdateAutoEmbeddings = async (
values: DeepRequired<AssistantFormValues> & { assistantID: string },
) => {
// remove any __typename from the form values
const payload = removeTypename(values);
if (values.webhooks.length === 0) {
delete payload.webhooks;
}
if (values.graphql.length === 0) {
delete payload.graphql;
}
// remove assistantId because the update mutation fails otherwise
delete payload.assistantID;
// If the assistantId is set then we do an update
if (assistantId) {
await updateAssistantMutation({
variables: {
id: assistantId,
data: payload,
},
});
return;
}
await insertAssistantMutation({
variables: {
data: {
...values,
},
},
});
};
const handleSubmit = async (
values: DeepRequired<AssistantFormValues> & { assistantID: string },
) => {
try {
await toast.promise(
createOrUpdateAutoEmbeddings(values),
{
loading: 'Configuring the Assistant...',
success: `The Assistant has been configured successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while configuring the Assistant. Please try again.'
);
},
},
getToastStyleProps(),
);
onSubmit?.();
} catch {
// Note: The toast will handle the error.
}
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col overflow-hidden border-t"
>
<div className="flex flex-1 flex-col space-y-4 overflow-auto p-4">
<Input
{...register('name')}
id="name"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Name</Text>
<Tooltip title="Name of the assistant">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.name}
helperText={errors?.name?.message}
fullWidth
autoComplete="off"
autoFocus
/>
<Input
{...register('description')}
id="description"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Description</Text>
<Tooltip title={<span>Description of the assistant</span>}>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.description}
helperText={errors?.description?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<Input
{...register('instructions')}
id="instructions"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Instructions</Text>
<Tooltip title="Instructions for the assistant. This is used to instruct the AI assistant on how to behave and respond to the user">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.instructions}
helperText={errors?.instructions?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<Input
{...register('model')}
id="model"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Model</Text>
<Tooltip title="Model to use for the assistant.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.model}
helperText={errors?.model?.message}
fullWidth
autoComplete="off"
autoFocus
/>
<GraphqlDataSourcesFormSection />
<WebhooksDataSourcesFormSection />
</div>
<Box className="flex w-full flex-row justify-between rounded border-t p-4">
<Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
startIcon={assistantId ? <ArrowsClockwise /> : <PlusIcon />}
>
{assistantId ? 'Update' : 'Create'}
</Button>
</Box>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1,165 @@
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { ControlledSwitch } from '@/components/form/ControlledSwitch';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
import { useFieldArray, useFormContext } from 'react-hook-form';
interface ArgumentsFormSectionProps {
nestedField: string;
nestIndex: number;
}
export default function ArgumentsFormSection({
nestedField,
nestIndex,
}: ArgumentsFormSectionProps) {
const form = useFormContext<AssistantFormValues>();
const {
register,
formState: { errors },
} = form;
const { fields, append, remove } = useFieldArray({
name: `${nestedField}.${nestIndex}.arguments`,
});
return (
<Box className="space-y-4">
<div className="flex flex-row items-center justify-between ">
<div className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Arguments
</Text>
<Tooltip title={<span>Arguments</span>}>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</div>
<Button variant="borderless" onClick={() => append({})}>
<PlusIcon className="h-5 w-5" />
</Button>
</div>
<div className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box
key={field.id}
className="flex flex-col space-y-20 rounded border-1 p-4"
sx={{ backgroundColor: 'grey.200' }}
>
<div className="flex w-full flex-col space-y-4">
<Input
// We're putting ts-ignore here so we could use the same components for both graphql and webhooks
// by passing the nestedField = 'graphql' or nestedField = 'webhooks'
{...register(
// @ts-ignore
`${nestedField}.${nestIndex}.arguments.${index}.name`,
)}
id={`${field.id}-name`}
placeholder="Name"
className="w-full"
hideEmptyHelperText
error={
!!errors?.[nestedField]?.[nestIndex]?.arguments[index].name
}
helperText={
errors?.[nestedField]?.[nestIndex]?.arguments[index]?.name
?.message
}
fullWidth
autoComplete="off"
/>
<Input
{...register(
// @ts-ignore
`${nestedField}.${nestIndex}.arguments.${index}.description`,
)}
id={`${field.id}-description`}
placeholder="Description"
className="w-full"
hideEmptyHelperText
error={
!!errors?.[nestedField]?.[nestIndex]?.arguments[index]
.description
}
helperText={
errors?.[nestedField]?.[nestIndex]?.arguments[index]
?.description?.message
}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<div className="flex flex-row space-x-2">
<Box className="w-full">
<ControlledSelect
fullWidth
{...register(
// @ts-ignore
`${nestedField}.${nestIndex}.arguments.${index}.type`,
)}
id={`${field.id}-type`}
placeholder="Select argument type"
slotProps={{
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
},
}}
>
{[
'string',
'number',
'integer',
'object',
'array',
'boolean',
]?.map((argumentType) => (
<Option key={argumentType} value={argumentType}>
{argumentType}
</Option>
))}
</ControlledSelect>
</Box>
<ControlledSwitch
{...register(
// @ts-ignore
`${nestedField}.${nestIndex}.arguments.${index}.required`,
)}
disabled={false}
label={
<Text variant="subtitle1" component="span">
Required
</Text>
}
/>
</div>
<Button
variant="borderless"
className="h-10 self-end"
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</div>
</Box>
))}
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,123 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
import { ArgumentsFormSection } from '@/features/ai/AssistantForm/components/ArgumentsFormSection';
import { useFieldArray, useFormContext } from 'react-hook-form';
export default function GraphqlDataSourcesFormSection() {
const form = useFormContext<AssistantFormValues>();
const {
register,
formState: { errors },
} = form;
const { fields, append, remove } = useFieldArray({
name: 'graphql',
});
return (
<Box className="space-y-4 rounded border-1">
<Box className="flex flex-row items-center justify-between p-4 pb-0">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
GraphQL
</Text>
<Tooltip title="GraphQL data sources and tools. Run against the project's GraphQL API">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() =>
append({
name: '',
description: '',
query: '',
arguments: [],
})
}
>
<PlusIcon className="h-5 w-5" />
</Button>
</Box>
<Box className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box key={field.id} className="flex flex-col space-y-4">
<Box className="flex w-full flex-col space-y-4 p-4 pt-0">
<Input
{...register(`graphql.${index}.name`)}
id={`${field.id}-name`}
label="Name"
placeholder="Name"
className="w-full"
hideEmptyHelperText
error={!!errors?.graphql?.at(index)?.name}
helperText={errors?.graphql?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register(`graphql.${index}.description`)}
id={`${field.id}-description`}
label="Description"
placeholder="Description"
className="w-full"
hideEmptyHelperText
error={!!errors?.graphql?.at(index)?.description}
helperText={errors?.graphql?.at(index)?.description?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<Input
{...register(`graphql.${index}.query`)}
id={`${field.id}-query`}
label="Query"
placeholder="Query"
className="w-full"
hideEmptyHelperText
error={!!errors?.graphql?.at(index)?.query}
helperText={errors?.graphql?.at(index)?.query?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<ArgumentsFormSection nestedField="graphql" nestIndex={index} />
<Button
variant="borderless"
className="h-10 self-end"
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</Box>
{index < fields.length - 1 && (
<Divider className="h-px" sx={{ background: 'grey.200' }} />
)}
</Box>
))}
</Box>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,123 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { type AssistantFormValues } from '@/features/ai/AssistantForm/AssistantForm';
import { ArgumentsFormSection } from '@/features/ai/AssistantForm/components/ArgumentsFormSection';
import { useFieldArray, useFormContext } from 'react-hook-form';
export default function WebhooksDataSourcesFormSection() {
const form = useFormContext<AssistantFormValues>();
const {
register,
formState: { errors },
} = form;
const { fields, append, remove } = useFieldArray({
name: 'webhooks',
});
return (
<Box className="space-y-4 rounded border-1">
<Box className="flex flex-row items-center justify-between p-4">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Webhooks
</Text>
<Tooltip title="Webhook data sources and tools">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() =>
append({
name: '',
description: '',
URL: '',
arguments: [],
})
}
>
<PlusIcon className="h-5 w-5" />
</Button>
</Box>
<Box className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box key={field.id} className="flex flex-col space-y-4">
<Box className="flex w-full flex-col space-y-4 p-4 pt-0">
<Input
{...register(`webhooks.${index}.name`)}
id={`${field.id}-name`}
label="Name"
placeholder="Name"
className="w-full"
hideEmptyHelperText
error={!!errors?.webhooks?.at(index)?.name}
helperText={errors?.webhooks?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register(`webhooks.${index}.description`)}
id={`${field.id}-description`}
label="Description"
placeholder="Description"
className="w-full"
hideEmptyHelperText
error={!!errors?.webhooks?.at(index)?.description}
helperText={errors?.webhooks?.at(index)?.description?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<Input
{...register(`webhooks.${index}.URL`)}
id={`${field.id}-URL`}
label="URL"
placeholder="URL"
className="w-full"
hideEmptyHelperText
error={!!errors?.webhooks?.at(index)?.URL}
helperText={errors?.webhooks?.at(index)?.URL?.message}
fullWidth
autoComplete="off"
multiline
inputProps={{
className: 'resize-y min-h-[22px]',
}}
/>
<ArgumentsFormSection nestedField="webhooks" nestIndex={index} />
<Button
variant="borderless"
className="h-10 self-end"
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</Box>
{index < fields.length - 1 && (
<Divider className="h-px" sx={{ background: 'grey.200' }} />
)}
</Box>
))}
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,158 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { AssistantForm } from '@/features/ai/AssistantForm';
import { DeleteAssistantModal } from '@/features/ai/DeleteAssistantModal';
import { copy } from '@/utils/copy';
import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
interface AssistantsListProps {
/**
* The run services fetched from entering the users page.
*/
assistants: Assistant[];
/**
* Function to be called after a submitting the form for either creating or updating a service.
*
* @example onDelete={() => refetch()}
*/
onCreateOrUpdate?: () => Promise<any>;
/**
* Function to be called after a successful delete action.
*
*/
onDelete?: () => Promise<any>;
}
export default function AssistantsList({
assistants,
onCreateOrUpdate,
onDelete,
}: AssistantsListProps) {
const { openDrawer, openDialog, closeDialog } = useDialog();
const viewAssistant = async (assistant: Assistant) => {
openDrawer({
title: `Edit ${assistant?.name ?? 'unset'}`,
component: (
<AssistantForm
assistantId={assistant.assistantID}
initialData={{
...assistant,
}}
onSubmit={() => onCreateOrUpdate()}
/>
),
});
};
const deleteAssistant = async (assistant: Assistant) => {
openDialog({
component: (
<DeleteAssistantModal
assistant={assistant}
close={closeDialog}
onDelete={onDelete}
/>
),
});
};
return (
<Box className="flex flex-col">
{assistants.map((assistant) => (
<Box
key={assistant.assistantID}
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
sx={{
[`&:hover`]: {
backgroundColor: 'action.hover',
},
}}
>
<Box
onClick={() => viewAssistant(assistant)}
className="flex w-full flex-row justify-between"
sx={{ backgroundColor: 'transparent' }}
>
<div className="flex flex-1 flex-row items-center space-x-4">
<span className="text-3xl">🤖</span>
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{assistant?.name ?? 'unset'}
</Text>
<div className="hidden flex-row items-center space-x-2 md:flex">
<Text variant="subtitle1" className="font-mono text-xs">
{assistant.assistantID}
</Text>
<IconButton
variant="borderless"
color="secondary"
onClick={(event) => {
copy(assistant.assistantID, 'Assistant Id');
event.stopPropagation();
}}
aria-label="Service Id"
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
</div>
</Box>
<Dropdown.Root>
<Dropdown.Trigger
asChild
hideChevron
onClick={(event) => event.stopPropagation()}
>
<IconButton
variant="borderless"
color="secondary"
aria-label="More options"
onClick={(event) => event.stopPropagation()}
>
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-auto' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Dropdown.Item
onClick={() => viewAssistant(assistant)}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="h-4 w-4" />
<Text className="font-medium">View {assistant?.name}</Text>
</Dropdown.Item>
<Divider component="li" />
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() => deleteAssistant(assistant)}
>
<TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error">
Delete {assistant?.name}
</Text>
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
</Box>
))}
</Box>
);
}

View File

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

View File

@@ -0,0 +1,330 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import {
useInsertGraphiteAutoEmbeddingsConfigurationMutation,
useUpdateGraphiteAutoEmbeddingsConfigurationMutation,
} from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
export const validationSchema = Yup.object({
name: Yup.string().required('The name is required.'),
schemaName: Yup.string().required('The schema is required'),
tableName: Yup.string().required('The table is required'),
columnName: Yup.string().required('The column is required'),
query: Yup.string(),
mutation: Yup.string(),
});
export type AutoEmbeddingsFormValues = Yup.InferType<typeof validationSchema>;
export interface AutoEmbeddingsFormProps extends DialogFormProps {
/**
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
*/
autoEmbeddingsId?: string;
/**
* if there is initialData then it's an update operation
*/
initialData?: AutoEmbeddingsFormValues;
/**
* Function to be called when the operation is cancelled.
*/
onCancel?: VoidFunction;
/**
* Function to be called when the submit is successful.
*/
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
}
export default function AutoEmbeddingsForm({
autoEmbeddingsId,
initialData,
onSubmit,
onCancel,
location,
}: AutoEmbeddingsFormProps) {
const { onDirtyStateChange } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [insertGraphiteAutoEmbeddingsConfiguration] =
useInsertGraphiteAutoEmbeddingsConfigurationMutation({
client,
});
const [updateGraphiteAutoEmbeddingsConfiguration] =
useUpdateGraphiteAutoEmbeddingsConfigurationMutation({ client });
const form = useForm<AutoEmbeddingsFormValues>({
defaultValues: initialData,
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
const {
register,
formState: { errors, isSubmitting, dirtyFields },
} = form;
const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const createOrUpdateAutoEmbeddings = async (
values: AutoEmbeddingsFormValues,
) => {
// If the autoEmbeddingsId is set then we do an update
if (autoEmbeddingsId) {
await updateGraphiteAutoEmbeddingsConfiguration({
variables: {
id: autoEmbeddingsId,
...values,
},
});
return;
}
await insertGraphiteAutoEmbeddingsConfiguration({
variables: values,
});
};
const handleSubmit = async (values: AutoEmbeddingsFormValues) => {
try {
await toast.promise(
createOrUpdateAutoEmbeddings(values),
{
loading: 'Configuring the Auto-Embeddings...',
success: `The Auto-Embeddings has been configured successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while configuring the Auto-Embeddings. Please try again.'
);
},
},
getToastStyleProps(),
);
onSubmit?.();
} catch {
// Note: The toast will handle the error.
}
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col gap-4 overflow-hidden"
>
<div className="flex flex-1 flex-col space-y-6 overflow-auto px-6">
<Input
{...register('name')}
id="name"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Name</Text>
<Tooltip title="Name of the Auto-Embeddings">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.name}
helperText={errors?.name?.message}
fullWidth
autoComplete="off"
autoFocus
/>
<Input
{...register('schemaName')}
id="schemaName"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Schema</Text>
<Tooltip title={<span>Schema where the table belongs to</span>}>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.schemaName}
helperText={errors?.schemaName?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register('tableName')}
id="tableName"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Table</Text>
<Tooltip title="Table Name">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.tableName}
helperText={errors?.tableName?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register('columnName')}
id="columnName"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Column</Text>
<Tooltip title="Column name">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.columnName}
helperText={errors?.columnName?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register('query')}
id="query"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Query</Text>
<Tooltip title="Query">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.query}
helperText={errors?.query?.message}
fullWidth
autoComplete="off"
multiline
rows={6}
/>
<Input
{...register('mutation')}
id="mutation"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Mutation</Text>
<Tooltip title="Mutation">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder=""
hideEmptyHelperText
error={!!errors.mutation}
helperText={errors?.mutation?.message}
fullWidth
autoComplete="off"
multiline
rows={6}
/>
</div>
<Box className="flex w-full flex-row justify-between rounded border-t px-6 py-4">
<Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel
</Button>
<Button
type="submit"
disabled={isSubmitting}
startIcon={autoEmbeddingsId ? <ArrowsClockwise /> : <PlusIcon />}
>
{autoEmbeddingsId ? 'Update' : 'Create'}
</Button>
</Box>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,172 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CubeIcon } from '@/components/ui/v2/icons/CubeIcon';
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
import { EmbeddingsIcon } from '@/components/ui/v2/icons/EmbeddingsIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { AutoEmbeddingsForm } from '@/features/ai/AutoEmbeddingsForm';
import { DeleteAutoEmbeddingsModal } from '@/features/ai/DeleteAutoEmbeddingsModal';
import { formatDistanceToNow } from 'date-fns';
import type { AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings';
interface AutoEmbeddingsConfigurationsListProps {
/**
* The run services fetched from entering the users page.
*/
autoEmbeddingsConfigurations: AutoEmbeddingsConfiguration[];
/**
* Function to be called after a submitting the form for either creating or updating a service.
*
* @example onDelete={() => refetch()}
*/
onCreateOrUpdate?: () => Promise<any>;
/**
* Function to be called after a successful delete action.
*
*/
onDelete?: () => Promise<any>;
}
export default function AutoEmbeddingsList({
autoEmbeddingsConfigurations,
onCreateOrUpdate,
onDelete,
}: AutoEmbeddingsConfigurationsListProps) {
const { openDrawer, openDialog, closeDialog } = useDialog();
const viewAutoEmbeddingsConfiguration = async (
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration,
) => {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<Text>Edit {autoEmbeddingsConfiguration?.name ?? 'unset'}</Text>
</Box>
),
component: (
<AutoEmbeddingsForm
autoEmbeddingsId={autoEmbeddingsConfiguration.id}
initialData={{
...autoEmbeddingsConfiguration,
}}
onSubmit={() => onCreateOrUpdate()}
/>
),
});
};
const deleteAutoEmbeddingsConfiguration = async (
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration,
) => {
openDialog({
component: (
<DeleteAutoEmbeddingsModal
autoEmbeddingsConfiguration={autoEmbeddingsConfiguration}
close={closeDialog}
onDelete={onDelete}
/>
),
});
};
return (
<Box className="flex flex-col">
{autoEmbeddingsConfigurations.map((autoEmbeddingsConfiguration) => (
<Box
key={autoEmbeddingsConfiguration.id}
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
sx={{
[`&:hover`]: {
backgroundColor: 'action.hover',
},
}}
>
<Box
onClick={() =>
viewAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
}
className="flex w-full flex-row justify-between"
sx={{
backgroundColor: 'transparent',
}}
>
<div className="flex flex-1 flex-row items-center space-x-4">
<EmbeddingsIcon className="h-5 w-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{autoEmbeddingsConfiguration?.name ?? 'unset'}
</Text>
<Tooltip title={autoEmbeddingsConfiguration.updatedAt}>
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
Updated{' '}
{formatDistanceToNow(
new Date(autoEmbeddingsConfiguration.updatedAt),
)}{' '}
ago
</span>
</Tooltip>
</div>
</div>
</Box>
<Dropdown.Root>
<Dropdown.Trigger
asChild
hideChevron
onClick={(event) => event.stopPropagation()}
>
<IconButton
variant="borderless"
color="secondary"
aria-label="More options"
onClick={(event) => event.stopPropagation()}
>
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-auto' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Dropdown.Item
onClick={() =>
viewAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="h-4 w-4" />
<Text className="font-medium">
View {autoEmbeddingsConfiguration?.name}
</Text>
</Dropdown.Item>
<Divider component="li" />
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() =>
deleteAutoEmbeddingsConfiguration(autoEmbeddingsConfiguration)
}
>
<TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error">
Delete {autoEmbeddingsConfiguration?.name}
</Text>
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
</Box>
))}
</Box>
);
}

View File

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

View File

@@ -0,0 +1,143 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { useDeleteAssistantMutation } from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeleteAssistantModalProps {
assistant: Assistant;
onDelete?: () => Promise<any>;
close: () => void;
}
export default function DeleteAssistantModal({
assistant,
onDelete,
close,
}: DeleteAssistantModalProps) {
const [remove, setRemove] = useState(false);
const [loadingRemove, setLoadingRemove] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [deleteAssistantMutation] = useDeleteAssistantMutation({
client,
});
const deleteAssistant = async () => {
await deleteAssistantMutation({
variables: {
id: assistant.assistantID,
},
});
await onDelete?.();
close();
};
async function handleClick() {
setLoadingRemove(true);
await toast.promise(
deleteAssistant(),
{
loading: 'Deleting the assistant...',
success: `The Assistant has been deleted successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting the Assistant. Please try again.'
);
},
},
getToastStyleProps(),
);
}
return (
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
Delete Assistant {assistant?.name}
</Text>
<Text variant="subtitle2">
Are you sure you want to delete this Assistant?
</Text>
<Text
variant="subtitle2"
className="font-bold"
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
>
This cannot be undone.
</Text>
<Box className="my-4">
<Checkbox
id="accept-1"
label={`I'm sure I want to delete ${assistant?.name}`}
className="py-2"
checked={remove}
onChange={(_event, checked) => setRemove(checked)}
aria-label="Confirm Delete Assistant"
/>
</Box>
<div className="grid grid-flow-row gap-2">
<Button
color="error"
onClick={handleClick}
disabled={!remove}
loading={loadingRemove}
>
Delete Assistant
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,145 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { useDeleteGraphiteAutoEmbeddingsConfigurationMutation } from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { type AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeleteAutoEmbeddingsModalProps {
autoEmbeddingsConfiguration: AutoEmbeddingsConfiguration;
onDelete?: () => Promise<any>;
close: () => void;
}
export default function DeleteAutoEmbeddingsModal({
autoEmbeddingsConfiguration,
onDelete,
close,
}: DeleteAutoEmbeddingsModalProps) {
const [remove, setRemove] = useState(false);
const [loadingRemove, setLoadingRemove] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [deleteAutoEmbeddingsConfiguration] =
useDeleteGraphiteAutoEmbeddingsConfigurationMutation({
client,
});
const deleteAutoEmbeddingsConfig = async () => {
await deleteAutoEmbeddingsConfiguration({
variables: {
id: autoEmbeddingsConfiguration.id,
},
});
await onDelete?.();
close();
};
async function handleClick() {
setLoadingRemove(true);
await toast.promise(
deleteAutoEmbeddingsConfig(),
{
loading: 'Deleting Auto-Embeddings Configuration...',
success: `The Auto-Embeddings Configuration has been deleted successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting the Auto-Embeddings Configuration. Please try again.'
);
},
},
getToastStyleProps(),
);
}
return (
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
Delete Auto-Embeddings Configuration{' '}
{autoEmbeddingsConfiguration?.name}
</Text>
<Text variant="subtitle2">
Are you sure you want to delete this Auto-Embeddings Configuration?
</Text>
<Text
variant="subtitle2"
className="font-bold"
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
>
This cannot be undone.
</Text>
<Box className="my-4">
<Checkbox
id="accept-1"
label={`I'm sure I want to delete ${autoEmbeddingsConfiguration?.name}`}
className="py-2"
checked={remove}
onChange={(_event, checked) => setRemove(checked)}
aria-label="Confirm Delete Auto-Embeddings Configuration"
/>
</Box>
<div className="grid grid-flow-row gap-2">
<Button
color="error"
onClick={handleClick}
disabled={!remove}
loading={loadingRemove}
>
Delete Auto-Embeddings Configuration
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,219 @@
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon';
import { Input } from '@/components/ui/v2/Input';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { MessagesList } from '@/features/ai/DevAssistant/components/MessagesList';
import {
messagesState,
projectMessagesState,
sessionIDState,
} from '@/features/ai/DevAssistant/state';
import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useSendDevMessageMutation,
useStartDevSessionMutation,
type SendDevMessageMutation,
} from '@/utils/__generated__/graphite.graphql';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
const MAX_THREAD_LENGTH = 50;
export type Message = Omit<
SendDevMessageMutation['graphite']['sendDevMessage']['messages'][0],
'__typename'
>;
export default function DevAssistant() {
const { currentProject, currentWorkspace } = useCurrentWorkspaceAndProject();
const [loading, setLoading] = useState(false);
const [userInput, setUserInput] = useState('');
const setMessages = useSetRecoilState(messagesState);
const messages = useRecoilValue(projectMessagesState(currentProject.id));
const [storedSessionID, setStoredSessionID] = useRecoilState(sessionIDState);
const { adminClient } = useAdminApolloClient();
const [startDevSession] = useStartDevSessionMutation({ client: adminClient });
const [sendDevMessage] = useSendDevMessageMutation({ client: adminClient });
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
setLoading(true);
setUserInput('');
let sessionID = storedSessionID;
const lastMessage = messages.slice(1).pop(); // The first message is a welcome message, so we exclude it
let hasBeenAnHourSinceLastMessage = false;
if (lastMessage) {
hasBeenAnHourSinceLastMessage =
new Date().getTime() - new Date(lastMessage.createdAt).getTime() >
60 * 60 * 1000;
}
const $messages = [
...messages,
{
id: String(new Date().getTime()),
message: userInput,
createdAt: null,
role: 'user',
projectId: currentProject.id,
},
];
setMessages($messages);
if (!sessionID || hasBeenAnHourSinceLastMessage) {
const sessionRes = await startDevSession({ client: adminClient });
sessionID = sessionRes?.data?.graphite?.startDevSession?.sessionID;
setStoredSessionID(sessionID);
}
if (!sessionID) {
throw new Error('Failed to start a new session');
}
const {
data: {
graphite: { sendDevMessage: { messages: newMessages } = {} } = {},
} = {},
} = await sendDevMessage({
variables: {
message: userInput,
sessionId: sessionID || '',
prevMessageID: !hasBeenAnHourSinceLastMessage
? lastMessage?.id || ''
: '',
},
});
let thread = [
// remove the temp messages of the user input while we wait for the dev assistant to respond
...$messages.filter((item) => item.createdAt),
...newMessages
// remove empty messages
.filter((item) => item.message)
// add the currentProject.id to the new messages
.map((item) => ({ ...item, projectId: currentProject.id })),
];
if (thread.length > MAX_THREAD_LENGTH) {
thread = thread.slice(thread.length - MAX_THREAD_LENGTH); // keep the thread at a max length of MAX_THREAD_LENGTH
}
setMessages(thread);
} catch (error) {
toast.error(
'Failed to send the message to graphite. Please try again later.',
{
style: getToastStyleProps().style,
...getToastStyleProps().error,
},
);
} finally {
setLoading(false);
}
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
const form = event.currentTarget.closest('form');
if (form) {
form.dispatchEvent(
new Event('submit', { bubbles: true, cancelable: true }),
);
}
}
};
if (currentProject.plan.isFree) {
return (
<Box className="p-4">
<UpgradeToProBanner
title="Upgrade to Nhost Pro."
description={
<Text>
Graphite is an addon to the Pro plan. To unlock it, please upgrade
to Pro first.
</Text>
}
/>
</Box>
);
}
if (!currentProject.plan.isFree && !currentProject.config?.ai) {
return (
<Box className="p-4">
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
<Text className="grid grid-flow-row justify-items-start gap-0.5">
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/ai`}
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
AI Settings
</Link>
.
</Text>
</Text>
</Alert>
</Box>
);
}
return (
<div className="flex h-full flex-col overflow-auto">
<MessagesList loading={loading} />
<form onSubmit={handleSubmit}>
<Box className="relative flex w-full flex-row justify-between p-2">
<Input
value={userInput}
onChange={(event) => {
const { value } = event.target;
setUserInput(value);
}}
onKeyPress={handleKeyPress}
placeholder="Ask graphite anything!"
className="w-full"
required
slotProps={{
input: { className: 'w-full rounded-none border-none' },
}}
multiline
maxRows={7}
/>
<IconButton
disabled={!userInput || loading}
color="primary"
aria-label="Send"
type="submit"
className="absolute right-2 h-10 w-12 self-end rounded-xl"
>
{loading ? <ActivityIndicator /> : <ArrowUpIcon />}
</IconButton>
</Box>
</form>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { Box } from '@/components/ui/v2/Box';
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
import { Text } from '@/components/ui/v2/Text';
export default function LoadingAssistantMessage() {
return (
<Box className="flex flex-col space-y-4 border-t p-4">
<div className="flex items-center space-x-2">
<GraphiteIcon />
<Text className="font-bold">Assistant</Text>
</div>
<div className="flex space-x-1">
<Box
className="h-1.5 w-1.5 animate-blinking rounded-full"
sx={{ backgroundColor: 'grey.600' }}
/>
<Box
className="h-1.5 w-1.5 animate-blinking rounded-full animate-delay-150"
sx={{ backgroundColor: 'grey.600' }}
/>
<Box
className="h-1.5 w-1.5 animate-blinking rounded-full animate-delay-300"
sx={{ backgroundColor: 'grey.600' }}
/>
</div>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,98 @@
import { Avatar } from '@/components/ui/v2/Avatar';
import { Box } from '@/components/ui/v2/Box';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { GraphiteIcon } from '@/components/ui/v2/icons/GraphiteIcon';
import { Text } from '@/components/ui/v2/Text';
import { type Message } from '@/features/ai/DevAssistant';
import { copy } from '@/utils/copy';
import { useTheme } from '@mui/material';
import { useUserData } from '@nhost/nextjs';
import { onlyText } from 'react-children-utilities';
import Markdown, { type ExtraProps } from 'react-markdown';
import rehypeHighlight from 'rehype-highlight';
import remarkGFM from 'remark-gfm';
import { twMerge } from 'tailwind-merge';
import { type ClassAttributes, type HTMLAttributes } from 'react';
function PreComponent(
props: ClassAttributes<HTMLElement> &
HTMLAttributes<HTMLElement> &
ExtraProps,
) {
const { children } = props;
return (
<div className="group relative">
<pre>{children}</pre>
<IconButton
sx={{
minWidth: 0,
padding: 0.5,
backgroundColor: 'grey.100',
}}
color="warning"
variant="contained"
className="absolute top-2 right-2 hidden group-hover:flex"
onClick={(e) => {
e.stopPropagation();
copy(onlyText(children), 'Snippet');
}}
>
<CopyIcon className="h-5 w-5" />
</IconButton>
</div>
);
}
export default function MessageBox({ message }: { message: Message }) {
const theme = useTheme();
const user = useUserData();
const isUserMessage = message.role === 'user';
return (
<Box
className="flex flex-col space-y-4 border-t p-4 first:border-t-0"
sx={{
backgroundColor: isUserMessage && 'background.default',
}}
>
<div className="flex items-center space-x-2">
{message.role === 'assistant' ? (
<>
<GraphiteIcon />
<Text className="font-bold">Assistant</Text>
</>
) : (
<>
<Avatar
className="h-7 w-7 rounded-full"
alt={user?.displayName}
src={user?.avatarUrl}
>
{user?.displayName || 'local'}
</Avatar>
<Text className="font-bold">
{user?.displayName || 'local'} (You)
</Text>
</>
)}
</div>
<Markdown
className={twMerge(
'prose',
theme.palette.mode === 'dark' && 'prose-invert',
)}
rehypePlugins={[rehypeHighlight]}
remarkPlugins={[remarkGFM]}
components={{
pre: PreComponent,
}}
>
{message.message}
</Markdown>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,36 @@
import { Box } from '@/components/ui/v2/Box';
import { LoadingAssistantMessage } from '@/features/ai/DevAssistant/components/LoadingAssistantMessage';
import { MessageBox } from '@/features/ai/DevAssistant/components/MessageBox';
import { projectMessagesState } from '@/features/ai/DevAssistant/state';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { memo, useEffect, useRef } from 'react';
import { useRecoilValue } from 'recoil';
interface MessagesListProps {
loading: boolean;
}
function MessagesList({ loading }: MessagesListProps) {
const bottomElement = useRef(null);
const { currentProject } = useCurrentWorkspaceAndProject();
const messages = useRecoilValue(projectMessagesState(currentProject.id));
const scrollToBottom = () =>
bottomElement?.current?.scrollIntoView({ behavior: 'instant' });
useEffect(() => {
scrollToBottom();
}, [messages, loading]);
return (
<Box className="flex grow flex-col overflow-auto border-y">
{messages.map((message) => (
<MessageBox key={message.id} message={message} />
))}
{loading && <LoadingAssistantMessage />}
<div ref={bottomElement} />
</Box>
);
}
export default memo(MessagesList);

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
export * from './messages';
export { default as messagesState } from './messages';
// eslint-disable-next-line import/no-cycle
export { default as projectMessagesState } from './projectMessages';
export { default as sessionIDState } from './session';

View File

@@ -0,0 +1,23 @@
import { type Message } from '@/features/ai/DevAssistant';
import { persistAtom } from '@/utils/recoil';
import { atom } from 'recoil';
export interface ProjectMessage extends Message {
projectId?: string;
}
const messagesState = atom<ProjectMessage[]>({
key: 'messages',
default: [
{
id: '0',
message:
"Hi, I'm your personal Nhost AI assistant. I'm here to help answer questions, assist with tasks, provide information, or just have a conversation about GraphQL!",
role: 'assistant',
createdAt: new Date().toISOString(),
},
],
effects: [persistAtom],
});
export default messagesState;

View File

@@ -0,0 +1,21 @@
import {
messagesState,
type ProjectMessage,
} from '@/features/ai/DevAssistant/state';
import { selectorFamily } from 'recoil';
const projectMessagesState = selectorFamily<ProjectMessage[], string>({
key: 'projectMessages',
get:
(projectId) =>
({ get }) => {
const messages = get(messagesState);
return messages.filter(
(message) =>
message.projectId === projectId || message.projectId === undefined,
);
},
});
export default projectMessagesState;

View File

@@ -0,0 +1,10 @@
import { persistAtom } from '@/utils/recoil';
import { atom } from 'recoil';
const sessionIDState = atom<string>({
key: 'sessionID',
default: '',
effects: [persistAtom],
});
export default sessionIDState;

View File

@@ -0,0 +1,423 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
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 { Alert } from '@/components/ui/v2/Alert';
import { filterOptions } from '@/components/ui/v2/Autocomplete';
import { Box } from '@/components/ui/v2/Box';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { Input } from '@/components/ui/v2/Input';
import { Switch } from '@/components/ui/v2/Switch';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection';
import {
Software_Type_Enum,
useGetAiSettingsQuery,
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
import { DisableAIServiceConfirmationDialog } from './DisableAIServiceConfirmationDialog';
const validationSchema = Yup.object({
version: Yup.object({
label: Yup.string().required(),
value: Yup.string().required(),
}),
webhookSecret: Yup.string(),
synchPeriodMinutes: Yup.number(),
organization: Yup.string(),
apiKey: Yup.string().required(),
compute: Yup.object({
cpu: Yup.number().required(),
memory: Yup.number().required(),
}),
});
export type AISettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function AISettings() {
const { maintenanceActive } = useUI();
const { openDialog } = useDialog();
const [updateConfig] = useUpdateConfigMutation();
const { currentProject } = useCurrentWorkspaceAndProject();
const [aiServiceEnabled, setAIServiceEnabled] = useState(true);
const {
data: { config: { ai } = {} } = {},
loading: loadingAiSettings,
error: errorGettingAiSettings,
} = useGetAiSettingsQuery({
variables: {
appId: currentProject.id,
},
});
const { data: graphiteVersionsData, loading: loadingGraphiteVersionsData } =
useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Graphite,
},
});
const graphiteVersions = graphiteVersionsData?.softwareVersions || [];
const availableVersionsSet = new Set(
graphiteVersions.map((el) => el.version),
);
if (ai?.version) {
availableVersionsSet.add(ai.version);
}
const availableVersions = Array.from(availableVersionsSet)
.sort()
.reverse()
.map((availableVersion) => ({
label: availableVersion,
value: availableVersion,
}));
const form = useForm<AISettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
version: {
label: '0.1.0-beta4',
value: '0.1.0-beta4',
},
webhookSecret: '',
organization: '',
apiKey: '',
synchPeriodMinutes: 5,
compute: {
cpu: 125,
memory: 256,
},
},
resolver: yupResolver(validationSchema),
});
const { register, formState, reset, watch } = form;
useEffect(() => {
if (ai) {
reset({
version: { label: ai?.version, value: ai?.version },
webhookSecret: ai?.webhookSecret,
synchPeriodMinutes: ai?.autoEmbeddings?.synchPeriodMinutes,
apiKey: ai?.openai?.apiKey,
organization: ai?.openai?.organization,
compute: {
cpu: ai?.resources?.compute?.cpu ?? 62,
memory: ai?.resources?.compute?.memory ?? 128,
},
});
}
setAIServiceEnabled(!!ai);
}, [ai, reset]);
const toggleAIService = async (enabled: boolean) => {
setAIServiceEnabled(enabled);
if (!enabled) {
openDialog({
title: 'Confirm Disabling the AI service',
component: (
<DisableAIServiceConfirmationDialog
onCancel={() => setAIServiceEnabled(true)}
onServiceDisabled={() => setAIServiceEnabled(false)}
/>
),
});
}
};
if (loadingAiSettings || loadingGraphiteVersionsData) {
return (
<ActivityIndicator
delay={1000}
label="Loading Postgres version..."
className="justify-center"
/>
);
}
if (errorGettingAiSettings) {
throw errorGettingAiSettings;
}
async function handleSubmit(formValues: AISettingsFormValues) {
try {
await toast.promise(
updateConfig({
variables: {
appId: currentProject.id,
config: {
ai: {
version: formValues.version.value,
webhookSecret: formValues.webhookSecret,
autoEmbeddings: {
synchPeriodMinutes: Number(formValues.synchPeriodMinutes),
},
openai: {
apiKey: formValues.apiKey,
organization: formValues.organization,
},
resources: {
compute: {
cpu: formValues?.compute?.cpu,
memory: formValues?.compute?.memory,
},
},
},
},
},
}),
{
loading: `AI settings are being updated...`,
success: `AI settings has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the AI settings!`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
}
const aiSettingsFormValues = watch();
const getAIResourcesCost = () => {
const vCPUs = `${
aiSettingsFormValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER
} vCPUs`;
const mem = `${aiSettingsFormValues.compute.memory} MiB Mem`;
const details = `${vCPUs} + ${mem}`;
return `Approximate cost for ${details}`;
};
return (
<Box className="space-y-4" sx={{ backgroundColor: 'background.default' }}>
<Box className="flex flex-row items-center justify-between rounded-lg border-1 p-4">
<Text className="text-lg font-semibold">Enable AI service</Text>
<Switch
checked={aiServiceEnabled}
onChange={(e) => toggleAIService(e.target.checked)}
className="self-center"
/>
</Box>
{aiServiceEnabled && (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title={null}
description={null}
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="flex flex-col"
>
<Box className="space-y-4">
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
<Text className="text-lg font-semibold">Version</Text>
<Tooltip title="Version of the service to use.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<ControlledAutocomplete
id="version"
name="version"
filterOptions={(options, state) => {
if (state.inputValue === ai?.version) {
return options;
}
return filterOptions(options, state);
}}
fullWidth
className="col-span-4"
options={availableVersions}
error={!!formState.errors?.version?.message}
helperText={formState.errors?.version?.message}
showCustomOption="auto"
customOptionLabel={(value) =>
`Use custom value: "${value}"`
}
/>
</Box>
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
<Text className="text-lg font-semibold">
Webhook Secret
</Text>
<Tooltip title="Used to validate requests between postgres and the AI service. The AI service will also include the header X-Graphite-Webhook-Secret with this value set when calling external webhooks so the source of the request can be validated.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<Input
{...register('webhookSecret')}
id="webhookSecret"
name="webhookSecret"
placeholder="Webhook Secret"
className="col-span-3"
fullWidth
hideEmptyHelperText
error={Boolean(formState.errors.webhookSecret?.message)}
helperText={formState.errors.webhookSecret?.message}
/>
</Box>
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
<Text className="text-lg font-semibold">Resources</Text>
<Tooltip title="Dedicated resources allocated for the service.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{getAIResourcesCost()}</span>
<b>
$
{parseFloat(
(
aiSettingsFormValues.compute.cpu * COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
<ComputeFormSection />
</Box>
<Box className="space-y-2">
<Text className="text-lg font-semibold">OpenAI</Text>
<Input
{...register('apiKey')}
name="apiKey"
placeholder="API Key"
id="apiKey"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>OpenAI API key</Text>
<Tooltip title="Key to use for authenticating API requests to OpenAI">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
className="col-span-3"
fullWidth
hideEmptyHelperText
error={Boolean(formState.errors.apiKey?.message)}
helperText={formState.errors.apiKey?.message}
/>
<Input
{...register('organization')}
id="organization"
name="organization"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>OpenAI Organization</Text>
<Tooltip title="Optional. OpenAI organization to use.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder="Organization"
className="col-span-3"
fullWidth
hideEmptyHelperText
error={Boolean(formState.errors.organization?.message)}
helperText={formState.errors.organization?.message}
/>
</Box>
<Box className="space-y-2">
<Text className="text-lg font-semibold">Auto-Embeddings</Text>
<Input
{...register('synchPeriodMinutes')}
id="synchPeriodMinutes"
name="synchPeriodMinutes"
type="number"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Synch Period Minutes</Text>
<Tooltip title="How often to run the job that keeps embeddings up to date.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder="Synch Period Minutes"
fullWidth
className="lg:col-span-2"
error={Boolean(
formState.errors.synchPeriodMinutes?.message,
)}
helperText={formState.errors.synchPeriodMinutes?.message}
slotProps={{
inputRoot: {
min: 0,
},
}}
/>
</Box>
</Box>
</SettingsContainer>
</Form>
</FormProvider>
)}
</Box>
);
}

View File

@@ -0,0 +1,105 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings';
import { useUpdateConfigMutation } from '@/utils/__generated__/graphql';
import type { ApolloError } from '@apollo/client';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DisableAIServiceConfirmationDialogProps {
/**
* Function to be called when the user clicks the cancel button.
*/
onCancel: () => void;
/**
* Function to be called when the user clicks the confirm button.
*/
onServiceDisabled: () => void;
}
export default function DisableAIServiceConfirmationDialog({
onCancel,
onServiceDisabled,
}: DisableAIServiceConfirmationDialogProps) {
const { closeDialog } = useDialog();
const [loading, setLoading] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
async function handleClick() {
setLoading(true);
await toast.promise(
updateConfig({
variables: {
appId: currentProject.id,
config: {
ai: null,
},
},
}),
{
loading: 'Disabling the AI service...',
success: () => {
onServiceDisabled();
closeDialog();
return `The service has been disabled.`;
},
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while disabling the AI service. Please try again later.'
);
},
},
getToastStyleProps(),
);
}
return (
<Box className={twMerge('w-full rounded-lg p-6 pt-0 text-left')}>
<div className="grid grid-flow-row gap-1">
<Text variant="subtitle2">
Are you sure you want to disable this service?
</Text>
<Text
variant="subtitle2"
className="font-bold"
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
>
This cannot be undone.
</Text>
<div className="grid grid-flow-row gap-2">
<Button color="error" onClick={handleClick} loading={loading}>
Disable
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => {
onCancel();
closeDialog();
}}
>
Cancel
</Button>
</div>
</div>
</Box>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
query GetAISettings($appId: uuid!) {
config(appID: $appId, resolve: false) {
ai {
version
webhookSecret
autoEmbeddings {
synchPeriodMinutes
}
openai {
apiKey
organization
}
resources {
compute {
cpu
memory
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,31 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getHasuraAdminSecret } from '@/utils/env';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
export default function useAdminApolloClient() {
const { currentProject } = useCurrentWorkspaceAndProject();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const adminClient = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
return {
adminClient,
};
}

View File

@@ -1,4 +1,5 @@
import { useUI } from '@/components/common/UIProvider';
import { AIIcon } from '@/components/ui/v2/icons/AIIcon';
import { CloudIcon } from '@/components/ui/v2/icons/CloudIcon';
import { CogIcon } from '@/components/ui/v2/icons/CogIcon';
import { DatabaseIcon } from '@/components/ui/v2/icons/DatabaseIcon';
@@ -144,6 +145,13 @@ export default function useProjectRoutes() {
icon: <ServicesIcon />,
disabled: !isPlatform,
},
{
relativeMainPath: '/ai',
relativePath: '/ai/auto-embeddings',
exact: false,
label: 'AI',
icon: <AIIcon />,
},
...nhostRoutes,
];

View File

@@ -1,7 +1,6 @@
import type { ProjectFragment } from '@/utils/__generated__/graphql';
import { test, vi } from 'vitest';
import generateAppServiceUrl, {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
} from './generateAppServiceUrl';
@@ -138,7 +137,7 @@ test('should be able to override the default remote backend slugs', () => {
process.env.NEXT_PUBLIC_ENV = 'production';
expect(
generateAppServiceUrl('test', region, 'hasura', defaultLocalBackendSlugs, {
generateAppServiceUrl('test', region, 'hasura', {
...defaultRemoteBackendSlugs,
hasura: '/lorem-ipsum',
}),
@@ -187,24 +186,3 @@ test('should construct service URLs based on environment variables', () => {
'https://localdev4.nhost.run/v1/functions',
);
});
test('should generate a basic subdomain with a custom port if provided', () => {
process.env.NEXT_PUBLIC_NHOST_BACKEND_URL = `http://localhost:1338`;
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
expect(generateAppServiceUrl('test', region, 'auth')).toBe(
`http://localhost:1338/v1/auth`,
);
expect(generateAppServiceUrl('test', region, 'storage')).toBe(
`http://localhost:1338/v1/files`,
);
expect(generateAppServiceUrl('test', region, 'graphql')).toBe(
`http://localhost:1338/v1/graphql`,
);
expect(generateAppServiceUrl('test', region, 'functions')).toBe(
`http://localhost:1338/v1/functions`,
);
});

View File

@@ -62,7 +62,6 @@ export default function generateAppServiceUrl(
subdomain: string,
region: ProjectFragment['region'],
service: NhostService,
localBackendSlugs = defaultLocalBackendSlugs,
remoteBackendSlugs = defaultRemoteBackendSlugs,
) {
const IS_PLATFORM = isPlatform();
@@ -87,12 +86,6 @@ export default function generateAppServiceUrl(
return serviceUrls[service];
}
// This is only used when running the dashboard locally against its own
// backend.
if (process.env.NEXT_PUBLIC_ENV === 'dev') {
return `${process.env.NEXT_PUBLIC_NHOST_BACKEND_URL}${localBackendSlugs[service]}`;
}
const constructedDomain = [
subdomain,
service,

View File

@@ -16,7 +16,6 @@ import { useAppClient } from '@/features/projects/common/hooks/useAppClient';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
generateAppServiceUrl,
} from '@/features/projects/common/utils/generateAppServiceUrl';
@@ -110,7 +109,6 @@ export default function SystemEnvironmentVariableSettings() {
currentProject?.subdomain,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
),
},

View File

@@ -345,7 +345,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>
@@ -385,7 +385,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="w-4 h-4"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
@@ -416,7 +416,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>
@@ -447,7 +447,7 @@ export default function ServiceForm({
</b>
</Alert>
<ComputeFormSection />
<ComputeFormSection showTooltip />
<ReplicasFormSection />
@@ -460,7 +460,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

@@ -14,7 +14,13 @@ import {
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { useFormContext, useWatch } from 'react-hook-form';
export default function ComputeFormSection() {
interface ComputeFormSectionProps {
showTooltip?: boolean;
}
export default function ComputeFormSection({
showTooltip = false,
}: ComputeFormSectionProps) {
const { setValue } = useFormContext<ServiceFormValues>();
const formValues = useWatch<ServiceFormValues>();
@@ -34,14 +40,18 @@ export default function ComputeFormSection() {
const incrementCompute = () => {
const newMemoryValue = formValues.compute.memory + 128;
setValue('compute.memory', newMemoryValue);
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO));
setValue('compute.memory', newMemoryValue, { shouldDirty: true });
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO), {
shouldDirty: true,
});
};
const decrementCompute = () => {
const newMemoryValue = formValues.compute.memory - 128;
setValue('compute.memory', newMemoryValue);
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO));
setValue('compute.memory', newMemoryValue, { shouldDirty: true });
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO), {
shouldDirty: true,
});
};
return (
@@ -52,24 +62,26 @@ export default function ComputeFormSection() {
{formValues.compute.memory}
</Text>
<Tooltip
title={
<span>
Compute resources dedicated for the service. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/resources"
className="underline"
>
resources
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
{showTooltip && (
<Tooltip
title={
<span>
Compute resources dedicated for the service. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/resources"
className="underline"
>
resources
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
)}
</Box>
<Box className="flex flex-row items-center justify-between space-x-4">

View File

@@ -8,5 +8,22 @@ mutation UpdateConfig($appId: uuid!, $config: ConfigConfigUpdateInput!) {
}
}
}
ai {
version
webhookSecret
autoEmbeddings {
synchPeriodMinutes
}
openai {
organization
apiKey
}
resources {
compute {
cpu
memory
}
}
}
}
}

View File

@@ -20,6 +20,9 @@ fragment Project on apps {
enableConsole
}
}
ai {
version
}
}
featureFlags {
description

View File

@@ -0,0 +1,5 @@
mutation deleteAssistant($id: String!) {
graphite {
deleteAssistant(assistantID: $id)
}
}

View File

@@ -0,0 +1,33 @@
query getAssistants {
graphite {
assistants {
assistantID
name
description
model
instructions
graphql {
name
query
description
arguments {
name
type
description
required
}
}
webhooks {
name
URL
description
arguments {
name
type
description
required
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
mutation insertAssistant($data: graphiteAssistantInput!) {
graphite {
insertAssistant(object: $data) {
assistantID
}
}
}

View File

@@ -0,0 +1,7 @@
mutation updateAssistant($id: String!, $data: graphiteAssistantInput!) {
graphite {
updateAssistant(assistantID: $id, object: $data) {
assistantID
}
}
}

View File

@@ -0,0 +1,5 @@
mutation deleteGraphiteAutoEmbeddingsConfiguration($id: uuid!) {
deleteGraphiteAutoEmbeddingsConfiguration(id: $id) {
__typename
}
}

View File

@@ -0,0 +1,19 @@
query getGraphiteAutoEmbeddingsConfigurations($limit: Int!, $offset: Int!) {
graphiteAutoEmbeddingsConfigurations(limit: $limit, offset: $offset) {
id
name
schemaName
tableName
columnName
query
mutation
createdAt
updatedAt
}
graphiteAutoEmbeddingsConfigurationAggregate {
aggregate {
count
}
}
}

View File

@@ -0,0 +1,21 @@
mutation insertGraphiteAutoEmbeddingsConfiguration(
$name: String
$schemaName: String
$tableName: String
$columnName: String
$query: String
$mutation: String
) {
insertGraphiteAutoEmbeddingsConfiguration(
object: {
name: $name
schemaName: $schemaName
tableName: $tableName
columnName: $columnName
query: $query
mutation: $mutation
}
) {
id
}
}

View File

@@ -0,0 +1,29 @@
mutation updateGraphiteAutoEmbeddingsConfiguration(
$id: uuid!
$name: String
$schemaName: String
$tableName: String
$columnName: String
$query: String
$mutation: String
) {
updateGraphiteAutoEmbeddingsConfiguration(
pk_columns: { id: $id }
_set: {
name: $name
schemaName: $schemaName
tableName: $tableName
columnName: $columnName
query: $query
mutation: $mutation
}
) {
id
name
schemaName
tableName
columnName
query
mutation
}
}

View File

@@ -0,0 +1,20 @@
mutation sendDevMessage(
$sessionId: String!
$prevMessageID: String!
$message: String!
) {
graphite {
sendDevMessage(
sessionID: $sessionId
prevMessageID: $prevMessageID
message: $message
) {
messages {
id
role
message
createdAt
}
}
}
}

View File

@@ -0,0 +1,7 @@
mutation startDevSession {
graphite {
startDevSession {
sessionID
}
}
}

View File

@@ -0,0 +1,160 @@
import { useDialog } from '@/components/common/DialogProvider';
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import AILayout from '@/components/layout/AILayout/AILayout';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { AssistantForm } from '@/features/ai/AssistantForm';
import { AssistantsList } from '@/features/ai/AssistantsList';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getHasuraAdminSecret } from '@/utils/env';
import {
useGetAssistantsQuery,
type GetAssistantsQuery,
} from '@/utils/__generated__/graphite.graphql';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { useMemo, type ReactElement } from 'react';
export type Assistant = Omit<
GetAssistantsQuery['graphite']['assistants'][0],
'__typename'
>;
export default function AssistantsPage() {
const { openDrawer } = useDialog();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const adminSecret = currentProject?.config?.hasura?.adminSecret;
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = useMemo(
() =>
new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: adminSecret,
},
}),
}),
[serviceUrl, adminSecret],
);
const { data, loading, refetch } = useGetAssistantsQuery({ client });
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
const openCreateAssistantForm = () => {
openDrawer({
title: 'Create a new Assistant',
component: <AssistantForm onSubmit={refetch} />,
});
};
if (currentProject.plan.isFree) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<UpgradeToProBanner
title="Upgrade to Nhost Pro."
description={
<Text>
Graphite is an addon to the Pro plan. To unlock it, please upgrade
to Pro first.
</Text>
}
/>
</Box>
);
}
if (!currentProject.plan.isFree && !currentProject.config?.ai) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
<Text className="grid grid-flow-row justify-items-start gap-0.5">
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/ai`}
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
AI Settings
</Link>
.
</Text>
</Text>
</Alert>
</Box>
);
}
if (data?.graphite?.assistants.length === 0 && !loading) {
return (
<Box className="p-6" sx={{ backgroundColor: 'background.default' }}>
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<span className="text-6xl">🤖</span>
<div className="flex flex-col space-y-1">
<Text className="text-center font-medium" variant="h3">
No Assistants are configured
</Text>
<Text variant="subtitle1" className="text-center">
All your assistants will be listed here.
</Text>
</div>
<div className="flex flex-row place-content-between rounded-lg ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateAssistantForm}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Create a new assistant
</Button>
</div>
</Box>
</Box>
);
}
return (
<Box className="flex flex-col overflow-hidden">
<Box className="flex flex-row place-content-end border-b-1 p-4">
<Button
variant="contained"
color="primary"
onClick={openCreateAssistantForm}
startIcon={<PlusIcon className="h-4 w-4" />}
>
New
</Button>
</Box>
<div>
<AssistantsList
assistants={assistants}
onDelete={() => refetch()}
onCreateOrUpdate={() => refetch()}
/>
</div>
</Box>
);
}
AssistantsPage.getLayout = function getLayout(page: ReactElement) {
return <AILayout>{page}</AILayout>;
};

View File

@@ -0,0 +1,233 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Pagination } from '@/components/common/Pagination';
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import AILayout from '@/components/layout/AILayout/AILayout';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { EmbeddingsIcon } from '@/components/ui/v2/icons/EmbeddingsIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { AutoEmbeddingsForm } from '@/features/ai/AutoEmbeddingsForm';
import { AutoEmbeddingsList } from '@/features/ai/AutoEmbeddingsList';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getHasuraAdminSecret } from '@/utils/env';
import {
useGetGraphiteAutoEmbeddingsConfigurationsQuery,
type GetGraphiteAutoEmbeddingsConfigurationsQuery,
} from '@/utils/__generated__/graphite.graphql';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
export type AutoEmbeddingsConfiguration = Omit<
GetGraphiteAutoEmbeddingsConfigurationsQuery['graphiteAutoEmbeddingsConfigurations'][0],
'__typename'
>;
export default function AutoEmbeddingsPage() {
const limit = useRef(25);
const router = useRouter();
const { openDrawer } = useDialog();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const adminSecret = currentProject?.config?.hasura?.adminSecret;
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = useMemo(
() =>
new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: adminSecret,
},
}),
}),
[serviceUrl, adminSecret],
);
const [currentPage, setCurrentPage] = useState(
parseInt(router.query.page as string, 10) || 1,
);
const [nrOfPages, setNrOfPages] = useState(0);
const offset = useMemo(() => currentPage - 1, [currentPage]);
const { data, loading, refetch } =
useGetGraphiteAutoEmbeddingsConfigurationsQuery({
client,
variables: {
limit: limit.current,
offset,
},
});
useEffect(() => {
if (loading) {
return;
}
const autoEmbeddingsCount =
data?.graphiteAutoEmbeddingsConfigurationAggregate?.aggregate.count ?? 0;
setNrOfPages(Math.ceil(autoEmbeddingsCount / limit.current));
}, [data, loading]);
const autoEmbeddingsConfigurations = useMemo(
() => data?.graphiteAutoEmbeddingsConfigurations || [],
[data],
);
const openCreateAutoEmbeddingsConfiguration = () => {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<Text>Create new Auto-Embeddings configuration</Text>
</Box>
),
component: <AutoEmbeddingsForm onSubmit={refetch} />,
});
};
if (currentProject.plan.isFree) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<UpgradeToProBanner
title="Upgrade to Nhost Pro."
description={
<Text>
Graphite is an addon to the Pro plan. To unlock it, please upgrade
to Pro first.
</Text>
}
/>
</Box>
);
}
if (!currentProject.plan.isFree && !currentProject.config?.ai) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
<Text className="grid grid-flow-row justify-items-start gap-0.5">
<Text component="span">
To enable graphite, configure the service first in{' '}
<Link
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/ai`}
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
AI Settings
</Link>
.
</Text>
</Text>
</Alert>
</Box>
);
}
if (data?.graphiteAutoEmbeddingsConfigurations.length === 0 && !loading) {
return (
<Box className="p-6" sx={{ backgroundColor: 'background.default' }}>
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<EmbeddingsIcon className="h-10 w-10" />
<div className="flex flex-col space-y-1">
<Text className="text-center font-medium" variant="h3">
No Auto-Embeddings are configured
</Text>
<Text variant="subtitle1" className="text-center">
All your configurations will be listed here.
</Text>
</div>
<div className="flex flex-row place-content-between rounded-lg ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateAutoEmbeddingsConfiguration}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add a new Auto-Embeddings Configuration
</Button>
</div>
</Box>
</Box>
);
}
return (
<Box className="flex flex-col overflow-hidden">
<Box className="flex flex-row place-content-end border-b-1 p-4">
<Button
variant="contained"
color="primary"
onClick={openCreateAutoEmbeddingsConfiguration}
startIcon={<PlusIcon className="h-4 w-4" />}
>
New
</Button>
</Box>
<div>
<AutoEmbeddingsList
autoEmbeddingsConfigurations={autoEmbeddingsConfigurations}
onDelete={() => refetch()}
onCreateOrUpdate={() => refetch()}
/>
<Pagination
className="px-2 py-4"
totalNrOfPages={nrOfPages}
currentPageNumber={currentPage}
totalNrOfElements={
data?.graphiteAutoEmbeddingsConfigurationAggregate?.aggregate
?.count ?? 0
}
itemsLabel="Auto-Embeddings Configurations"
elementsPerPage={limit.current}
onPrevPageClick={async () => {
setCurrentPage((page) => page - 1);
if (currentPage - 1 !== 1) {
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage - 1 },
});
}
}}
onNextPageClick={async () => {
setCurrentPage((page) => page + 1);
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage + 1 },
});
}}
onPageChange={async (page) => {
setCurrentPage(page);
await router.push({
pathname: router.pathname,
query: { ...router.query, page },
});
}}
/>
</div>
</Box>
);
}
AutoEmbeddingsPage.getLayout = function getLayout(page: ReactElement) {
return <AILayout>{page}</AILayout>;
};

View File

@@ -11,7 +11,6 @@ import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
generateAppServiceUrl,
} from '@/features/projects/common/utils/generateAppServiceUrl';
@@ -39,7 +38,6 @@ export default function HasuraPage() {
currentProject?.subdomain,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
);

View File

@@ -0,0 +1,56 @@
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import { Container } from '@/components/layout/Container';
import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { AISettings } from '@/features/ai/settings/components';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { ReactElement } from 'react';
export default function StorageSettingsPage() {
const { currentProject, loading, error } = useCurrentWorkspaceAndProject();
if (currentProject.plan.isFree) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<UpgradeToProBanner
title="Upgrade to Nhost Pro."
description={
<Text>
Graphite is an addon to the Pro plan. To unlock it, please upgrade
to Pro first.
</Text>
}
/>
</Box>
);
}
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading AI settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
return (
<Container
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
rootClassName="bg-transparent"
>
<AISettings />
</Container>
);
}
StorageSettingsPage.getLayout = function getLayout(page: ReactElement) {
return <SettingsLayout>{page}</SettingsLayout>;
};

View File

@@ -24,9 +24,7 @@ export default function CustomDomains() {
>
<UpgradeToProBanner
title="Upgrade to Nhost Pro to unlock custom domains"
description="In publishing and graphic design, Lorem ipsum is a placeholder text
commonly used to demonstrate the visual form of a document or a
typeface without relying on meaningful content."
description=""
/>
</Container>
);
@@ -37,7 +35,7 @@ export default function CustomDomains() {
className="grid max-w-5xl grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<Box className="flex flex-row items-center gap-4 p-4 overflow-hidden rounded-lg border-1">
<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">Custom Domains</Text>
@@ -52,7 +50,7 @@ export default function CustomDomains() {
className="ml-1 font-medium"
>
Custom Domains
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
</div>

View File

@@ -4,6 +4,7 @@ import { RetryableErrorBoundary } from '@/components/presentational/RetryableErr
import { ThemeProvider } from '@/components/ui/v2/ThemeProvider';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import '@/styles/fonts.css';
import '@/styles/github-dark.css';
import '@/styles/globals.css';
import '@/styles/graphiql.min.css';
import '@/styles/style.css';
@@ -30,6 +31,7 @@ import Script from 'next/script';
import type { ReactElement } from 'react';
import { useEffect } from 'react';
import { Toaster } from 'react-hot-toast';
import { RecoilRoot } from 'recoil';
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
@@ -104,9 +106,11 @@ function MyApp({
colorPreferenceStorageKey={COLOR_PREFERENCE_STORAGE_KEY}
>
<RetryableErrorBoundary>
<DialogProvider>
{getLayout(<Component {...pageProps} />)}
</DialogProvider>
<RecoilRoot>
<DialogProvider>
{getLayout(<Component {...pageProps} />)}
</DialogProvider>
</RecoilRoot>
</RetryableErrorBoundary>
</ThemeProvider>
</UIProvider>

View File

@@ -0,0 +1,126 @@
/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/
pre:has(.hljs),
.hljs {
color: #c9d1d9;
background: #0d1117;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #ff7b72;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #d2a8ff;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #79c0ff;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #a5d6ff;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #ffa657;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #8b949e;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #7ee787;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #c9d1d9;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #1f6feb;
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #f2cc60;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #c9d1d9;
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #c9d1d9;
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #aff5b4;
background-color: #033a16;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffdcd7;
background-color: #67060c;
}
.hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
/* purposely ignored */
}

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,106 @@ export type Boolean_Comparison_Exp = {
_nin?: InputMaybe<Array<Scalars['Boolean']>>;
};
export type ConfigAi = {
__typename?: 'ConfigAI';
autoEmbeddings?: Maybe<ConfigAiAutoEmbeddings>;
openai: ConfigAiOpenai;
resources: ConfigAiResources;
version?: Maybe<Scalars['String']>;
webhookSecret: Scalars['String'];
};
export type ConfigAiAutoEmbeddings = {
__typename?: 'ConfigAIAutoEmbeddings';
synchPeriodMinutes?: Maybe<Scalars['ConfigUint32']>;
};
export type ConfigAiAutoEmbeddingsComparisonExp = {
_and?: InputMaybe<Array<ConfigAiAutoEmbeddingsComparisonExp>>;
_not?: InputMaybe<ConfigAiAutoEmbeddingsComparisonExp>;
_or?: InputMaybe<Array<ConfigAiAutoEmbeddingsComparisonExp>>;
synchPeriodMinutes?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigAiAutoEmbeddingsInsertInput = {
synchPeriodMinutes?: InputMaybe<Scalars['ConfigUint32']>;
};
export type ConfigAiAutoEmbeddingsUpdateInput = {
synchPeriodMinutes?: InputMaybe<Scalars['ConfigUint32']>;
};
export type ConfigAiComparisonExp = {
_and?: InputMaybe<Array<ConfigAiComparisonExp>>;
_not?: InputMaybe<ConfigAiComparisonExp>;
_or?: InputMaybe<Array<ConfigAiComparisonExp>>;
autoEmbeddings?: InputMaybe<ConfigAiAutoEmbeddingsComparisonExp>;
openai?: InputMaybe<ConfigAiOpenaiComparisonExp>;
resources?: InputMaybe<ConfigAiResourcesComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>;
webhookSecret?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigAiInsertInput = {
autoEmbeddings?: InputMaybe<ConfigAiAutoEmbeddingsInsertInput>;
openai: ConfigAiOpenaiInsertInput;
resources: ConfigAiResourcesInsertInput;
version?: InputMaybe<Scalars['String']>;
webhookSecret: Scalars['String'];
};
export type ConfigAiOpenai = {
__typename?: 'ConfigAIOpenai';
apiKey: Scalars['String'];
organization?: Maybe<Scalars['String']>;
};
export type ConfigAiOpenaiComparisonExp = {
_and?: InputMaybe<Array<ConfigAiOpenaiComparisonExp>>;
_not?: InputMaybe<ConfigAiOpenaiComparisonExp>;
_or?: InputMaybe<Array<ConfigAiOpenaiComparisonExp>>;
apiKey?: InputMaybe<ConfigStringComparisonExp>;
organization?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigAiOpenaiInsertInput = {
apiKey: Scalars['String'];
organization?: InputMaybe<Scalars['String']>;
};
export type ConfigAiOpenaiUpdateInput = {
apiKey?: InputMaybe<Scalars['String']>;
organization?: InputMaybe<Scalars['String']>;
};
export type ConfigAiResources = {
__typename?: 'ConfigAIResources';
compute: ConfigComputeResources;
};
export type ConfigAiResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigAiResourcesComparisonExp>>;
_not?: InputMaybe<ConfigAiResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigAiResourcesComparisonExp>>;
compute?: InputMaybe<ConfigComputeResourcesComparisonExp>;
};
export type ConfigAiResourcesInsertInput = {
compute: ConfigComputeResourcesInsertInput;
};
export type ConfigAiResourcesUpdateInput = {
compute?: InputMaybe<ConfigComputeResourcesUpdateInput>;
};
export type ConfigAiUpdateInput = {
autoEmbeddings?: InputMaybe<ConfigAiAutoEmbeddingsUpdateInput>;
openai?: InputMaybe<ConfigAiOpenaiUpdateInput>;
resources?: InputMaybe<ConfigAiResourcesUpdateInput>;
version?: InputMaybe<Scalars['String']>;
webhookSecret?: InputMaybe<Scalars['String']>;
};
export type ConfigAppConfig = {
__typename?: 'ConfigAppConfig';
appID: Scalars['uuid'];
@@ -951,9 +1051,38 @@ export type ConfigClaimMapUpdateInput = {
value?: InputMaybe<Scalars['String']>;
};
/** Resource configuration for a service */
export type ConfigComputeResources = {
__typename?: 'ConfigComputeResources';
/** milicpus, 1000 milicpus = 1 cpu */
cpu: Scalars['ConfigUint32'];
/** MiB: 128MiB to 30GiB */
memory: Scalars['ConfigUint32'];
};
export type ConfigComputeResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigComputeResourcesComparisonExp>>;
_not?: InputMaybe<ConfigComputeResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigComputeResourcesComparisonExp>>;
cpu?: InputMaybe<ConfigUint32ComparisonExp>;
memory?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigComputeResourcesInsertInput = {
cpu: Scalars['ConfigUint32'];
memory: Scalars['ConfigUint32'];
};
export type ConfigComputeResourcesUpdateInput = {
cpu?: InputMaybe<Scalars['ConfigUint32']>;
memory?: InputMaybe<Scalars['ConfigUint32']>;
};
/** main entrypoint to the configuration */
export type ConfigConfig = {
__typename?: 'ConfigConfig';
/** Configuration for graphite service */
ai?: Maybe<ConfigAi>;
/** Configuration for auth service */
auth?: Maybe<ConfigAuth>;
/** Configuration for functions service */
@@ -976,6 +1105,7 @@ export type ConfigConfigComparisonExp = {
_and?: InputMaybe<Array<ConfigConfigComparisonExp>>;
_not?: InputMaybe<ConfigConfigComparisonExp>;
_or?: InputMaybe<Array<ConfigConfigComparisonExp>>;
ai?: InputMaybe<ConfigAiComparisonExp>;
auth?: InputMaybe<ConfigAuthComparisonExp>;
functions?: InputMaybe<ConfigFunctionsComparisonExp>;
global?: InputMaybe<ConfigGlobalComparisonExp>;
@@ -987,6 +1117,7 @@ export type ConfigConfigComparisonExp = {
};
export type ConfigConfigInsertInput = {
ai?: InputMaybe<ConfigAiInsertInput>;
auth?: InputMaybe<ConfigAuthInsertInput>;
functions?: InputMaybe<ConfigFunctionsInsertInput>;
global?: InputMaybe<ConfigGlobalInsertInput>;
@@ -998,6 +1129,7 @@ export type ConfigConfigInsertInput = {
};
export type ConfigConfigUpdateInput = {
ai?: InputMaybe<ConfigAiUpdateInput>;
auth?: InputMaybe<ConfigAuthUpdateInput>;
functions?: InputMaybe<ConfigFunctionsUpdateInput>;
global?: InputMaybe<ConfigGlobalUpdateInput>;
@@ -1318,6 +1450,34 @@ export type ConfigHasuraUpdateInput = {
webhookSecret?: InputMaybe<Scalars['String']>;
};
export type ConfigHealthCheck = {
__typename?: 'ConfigHealthCheck';
initialDelaySeconds?: Maybe<Scalars['Int']>;
port: Scalars['ConfigPort'];
probePeriodSeconds?: Maybe<Scalars['Int']>;
};
export type ConfigHealthCheckComparisonExp = {
_and?: InputMaybe<Array<ConfigHealthCheckComparisonExp>>;
_not?: InputMaybe<ConfigHealthCheckComparisonExp>;
_or?: InputMaybe<Array<ConfigHealthCheckComparisonExp>>;
initialDelaySeconds?: InputMaybe<ConfigIntComparisonExp>;
port?: InputMaybe<ConfigPortComparisonExp>;
probePeriodSeconds?: InputMaybe<ConfigIntComparisonExp>;
};
export type ConfigHealthCheckInsertInput = {
initialDelaySeconds?: InputMaybe<Scalars['Int']>;
port: Scalars['ConfigPort'];
probePeriodSeconds?: InputMaybe<Scalars['Int']>;
};
export type ConfigHealthCheckUpdateInput = {
initialDelaySeconds?: InputMaybe<Scalars['Int']>;
port?: InputMaybe<Scalars['ConfigPort']>;
probePeriodSeconds?: InputMaybe<Scalars['Int']>;
};
export type ConfigIngress = {
__typename?: 'ConfigIngress';
fqdn?: Maybe<Array<Scalars['String']>>;
@@ -1549,12 +1709,15 @@ export type ConfigPostgresSettings = {
maxParallelMaintenanceWorkers?: Maybe<Scalars['ConfigInt32']>;
maxParallelWorkers?: Maybe<Scalars['ConfigInt32']>;
maxParallelWorkersPerGather?: Maybe<Scalars['ConfigInt32']>;
maxReplicationSlots?: Maybe<Scalars['ConfigInt32']>;
maxWalSenders?: Maybe<Scalars['ConfigInt32']>;
maxWalSize?: Maybe<Scalars['String']>;
maxWorkerProcesses?: Maybe<Scalars['ConfigInt32']>;
minWalSize?: Maybe<Scalars['String']>;
randomPageCost?: Maybe<Scalars['Float']>;
sharedBuffers?: Maybe<Scalars['String']>;
walBuffers?: Maybe<Scalars['String']>;
walLevel?: Maybe<Scalars['String']>;
workMem?: Maybe<Scalars['String']>;
};
@@ -1573,12 +1736,15 @@ export type ConfigPostgresSettingsComparisonExp = {
maxParallelMaintenanceWorkers?: InputMaybe<ConfigInt32ComparisonExp>;
maxParallelWorkers?: InputMaybe<ConfigInt32ComparisonExp>;
maxParallelWorkersPerGather?: InputMaybe<ConfigInt32ComparisonExp>;
maxReplicationSlots?: InputMaybe<ConfigInt32ComparisonExp>;
maxWalSenders?: InputMaybe<ConfigInt32ComparisonExp>;
maxWalSize?: InputMaybe<ConfigStringComparisonExp>;
maxWorkerProcesses?: InputMaybe<ConfigInt32ComparisonExp>;
minWalSize?: InputMaybe<ConfigStringComparisonExp>;
randomPageCost?: InputMaybe<ConfigFloatComparisonExp>;
sharedBuffers?: InputMaybe<ConfigStringComparisonExp>;
walBuffers?: InputMaybe<ConfigStringComparisonExp>;
walLevel?: InputMaybe<ConfigStringComparisonExp>;
workMem?: InputMaybe<ConfigStringComparisonExp>;
};
@@ -1594,12 +1760,15 @@ export type ConfigPostgresSettingsInsertInput = {
maxParallelMaintenanceWorkers?: InputMaybe<Scalars['ConfigInt32']>;
maxParallelWorkers?: InputMaybe<Scalars['ConfigInt32']>;
maxParallelWorkersPerGather?: InputMaybe<Scalars['ConfigInt32']>;
maxReplicationSlots?: InputMaybe<Scalars['ConfigInt32']>;
maxWalSenders?: InputMaybe<Scalars['ConfigInt32']>;
maxWalSize?: InputMaybe<Scalars['String']>;
maxWorkerProcesses?: InputMaybe<Scalars['ConfigInt32']>;
minWalSize?: InputMaybe<Scalars['String']>;
randomPageCost?: InputMaybe<Scalars['Float']>;
sharedBuffers?: InputMaybe<Scalars['String']>;
walBuffers?: InputMaybe<Scalars['String']>;
walLevel?: InputMaybe<Scalars['String']>;
workMem?: InputMaybe<Scalars['String']>;
};
@@ -1615,12 +1784,15 @@ export type ConfigPostgresSettingsUpdateInput = {
maxParallelMaintenanceWorkers?: InputMaybe<Scalars['ConfigInt32']>;
maxParallelWorkers?: InputMaybe<Scalars['ConfigInt32']>;
maxParallelWorkersPerGather?: InputMaybe<Scalars['ConfigInt32']>;
maxReplicationSlots?: InputMaybe<Scalars['ConfigInt32']>;
maxWalSenders?: InputMaybe<Scalars['ConfigInt32']>;
maxWalSize?: InputMaybe<Scalars['String']>;
maxWorkerProcesses?: InputMaybe<Scalars['ConfigInt32']>;
minWalSize?: InputMaybe<Scalars['String']>;
randomPageCost?: InputMaybe<Scalars['Float']>;
sharedBuffers?: InputMaybe<Scalars['String']>;
walBuffers?: InputMaybe<Scalars['String']>;
walLevel?: InputMaybe<Scalars['String']>;
workMem?: InputMaybe<Scalars['String']>;
};
@@ -1735,6 +1907,7 @@ export type ConfigRunServiceConfig = {
__typename?: 'ConfigRunServiceConfig';
command?: Maybe<Array<Scalars['String']>>;
environment?: Maybe<Array<ConfigEnvironmentVariable>>;
healthCheck?: Maybe<ConfigHealthCheck>;
image: ConfigRunServiceImage;
name: Scalars['ConfigRunServiceName'];
ports?: Maybe<Array<ConfigRunServicePort>>;
@@ -1747,6 +1920,7 @@ export type ConfigRunServiceConfigComparisonExp = {
_or?: InputMaybe<Array<ConfigRunServiceConfigComparisonExp>>;
command?: InputMaybe<ConfigStringComparisonExp>;
environment?: InputMaybe<ConfigEnvironmentVariableComparisonExp>;
healthCheck?: InputMaybe<ConfigHealthCheckComparisonExp>;
image?: InputMaybe<ConfigRunServiceImageComparisonExp>;
name?: InputMaybe<ConfigRunServiceNameComparisonExp>;
ports?: InputMaybe<ConfigRunServicePortComparisonExp>;
@@ -1756,6 +1930,7 @@ export type ConfigRunServiceConfigComparisonExp = {
export type ConfigRunServiceConfigInsertInput = {
command?: InputMaybe<Array<Scalars['String']>>;
environment?: InputMaybe<Array<ConfigEnvironmentVariableInsertInput>>;
healthCheck?: InputMaybe<ConfigHealthCheckInsertInput>;
image: ConfigRunServiceImageInsertInput;
name: Scalars['ConfigRunServiceName'];
ports?: InputMaybe<Array<ConfigRunServicePortInsertInput>>;
@@ -1765,6 +1940,7 @@ export type ConfigRunServiceConfigInsertInput = {
export type ConfigRunServiceConfigUpdateInput = {
command?: InputMaybe<Array<Scalars['String']>>;
environment?: InputMaybe<Array<ConfigEnvironmentVariableUpdateInput>>;
healthCheck?: InputMaybe<ConfigHealthCheckUpdateInput>;
image?: InputMaybe<ConfigRunServiceImageUpdateInput>;
name?: InputMaybe<Scalars['ConfigRunServiceName']>;
ports?: InputMaybe<Array<ConfigRunServicePortUpdateInput>>;
@@ -1839,7 +2015,7 @@ export type ConfigRunServicePortUpdateInput = {
/** Resource configuration for a service */
export type ConfigRunServiceResources = {
__typename?: 'ConfigRunServiceResources';
compute: ConfigRunServiceResourcesCompute;
compute: ConfigComputeResources;
/** Number of replicas for a service */
replicas: Scalars['ConfigUint8'];
storage?: Maybe<Array<ConfigRunServiceResourcesStorage>>;
@@ -1849,39 +2025,13 @@ export type ConfigRunServiceResourcesComparisonExp = {
_and?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
_not?: InputMaybe<ConfigRunServiceResourcesComparisonExp>;
_or?: InputMaybe<Array<ConfigRunServiceResourcesComparisonExp>>;
compute?: InputMaybe<ConfigRunServiceResourcesComputeComparisonExp>;
compute?: InputMaybe<ConfigComputeResourcesComparisonExp>;
replicas?: InputMaybe<ConfigUint8ComparisonExp>;
storage?: InputMaybe<ConfigRunServiceResourcesStorageComparisonExp>;
};
export type ConfigRunServiceResourcesCompute = {
__typename?: 'ConfigRunServiceResourcesCompute';
/** milicpus, 1000 milicpus = 1 cpu */
cpu: Scalars['ConfigUint32'];
/** MiB: 128MiB to 30GiB */
memory: Scalars['ConfigUint32'];
};
export type ConfigRunServiceResourcesComputeComparisonExp = {
_and?: InputMaybe<Array<ConfigRunServiceResourcesComputeComparisonExp>>;
_not?: InputMaybe<ConfigRunServiceResourcesComputeComparisonExp>;
_or?: InputMaybe<Array<ConfigRunServiceResourcesComputeComparisonExp>>;
cpu?: InputMaybe<ConfigUint32ComparisonExp>;
memory?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigRunServiceResourcesComputeInsertInput = {
cpu: Scalars['ConfigUint32'];
memory: Scalars['ConfigUint32'];
};
export type ConfigRunServiceResourcesComputeUpdateInput = {
cpu?: InputMaybe<Scalars['ConfigUint32']>;
memory?: InputMaybe<Scalars['ConfigUint32']>;
};
export type ConfigRunServiceResourcesInsertInput = {
compute: ConfigRunServiceResourcesComputeInsertInput;
compute: ConfigComputeResourcesInsertInput;
replicas: Scalars['ConfigUint8'];
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageInsertInput>>;
};
@@ -1917,7 +2067,7 @@ export type ConfigRunServiceResourcesStorageUpdateInput = {
};
export type ConfigRunServiceResourcesUpdateInput = {
compute?: InputMaybe<ConfigRunServiceResourcesComputeUpdateInput>;
compute?: InputMaybe<ConfigComputeResourcesUpdateInput>;
replicas?: InputMaybe<Scalars['ConfigUint8']>;
storage?: InputMaybe<Array<ConfigRunServiceResourcesStorageUpdateInput>>;
};
@@ -16470,6 +16620,7 @@ export type Query_RootGithubRepositoryArgs = {
export type Query_RootLogsArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
regexFilter?: InputMaybe<Scalars['String']>;
service?: InputMaybe<Scalars['String']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
@@ -17981,6 +18132,8 @@ export enum Software_Type_Constraint {
export enum Software_Type_Enum {
/** Hasura Auth */
Auth = 'Auth',
/** Graphite */
Graphite = 'Graphite',
/** Hasura GraphQL Engine */
Hasura = 'Hasura',
/** PostgreSQL Database */
@@ -19540,6 +19693,7 @@ export type Subscription_RootGithubRepositoryArgs = {
export type Subscription_RootLogsArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
regexFilter?: InputMaybe<Scalars['String']>;
service?: InputMaybe<Scalars['String']>;
};
@@ -22249,6 +22403,13 @@ export type DeletePersonalAccessTokenMutationVariables = Exact<{
export type DeletePersonalAccessTokenMutation = { __typename?: 'mutation_root', deletePersonalAccessToken?: { __typename?: 'authRefreshTokens', id: any, metadata?: any | null } | null };
export type GetAiSettingsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type GetAiSettingsQuery = { __typename?: 'query_root', config?: { __typename?: 'ConfigConfig', ai?: { __typename?: 'ConfigAI', version?: string | null, webhookSecret: string, autoEmbeddings?: { __typename?: 'ConfigAIAutoEmbeddings', synchPeriodMinutes?: any | null } | null, openai: { __typename?: 'ConfigAIOpenai', apiKey: string, organization?: string | null }, resources: { __typename?: 'ConfigAIResources', compute: { __typename?: 'ConfigComputeResources', cpu: any, memory: any } } } | null } | null };
export type GetAuthenticationSettingsQueryVariables = Exact<{
appId: Scalars['uuid'];
}>;
@@ -22329,7 +22490,7 @@ export type DeleteApplicationMutation = { __typename?: 'mutation_root', deleteAp
export type GetAllWorkspacesAndProjectsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetAllWorkspacesAndProjectsQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetAppPlanAndGlobalPlansAppFragment = { __typename?: 'apps', id: any, subdomain: string, workspace: { __typename?: 'workspaces', id: any, paymentMethods: Array<{ __typename?: 'paymentMethods', id: any }> }, plan: { __typename?: 'plans', id: any, name: string } };
@@ -22386,7 +22547,7 @@ export type GetWorkspaceAndProjectQueryVariables = Exact<{
}>;
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }> };
export type InsertApplicationMutationVariables = Exact<{
app: Apps_Insert_Input;
@@ -22493,7 +22654,7 @@ export type UpdateConfigMutationVariables = Exact<{
}>;
export type UpdateConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigPostgresResources', storage?: { __typename?: 'ConfigPostgresStorage', capacity: any } | null } | null } | null } };
export type UpdateConfigMutation = { __typename?: 'mutation_root', updateConfig: { __typename?: 'ConfigConfig', id: 'ConfigConfig', postgres?: { __typename?: 'ConfigPostgres', resources?: { __typename?: 'ConfigPostgresResources', 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 UnpauseApplicationMutationVariables = Exact<{
appId: Scalars['uuid'];
@@ -22578,9 +22739,9 @@ export type GetFilesAggregateQuery = { __typename?: 'query_root', filesAggregate
export type AppStateHistoryFragment = { __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, creatorUserId?: any | null, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', observability: { __typename?: 'ConfigObservability', grafana: { __typename?: 'ConfigGrafana', adminPassword: string } }, hasura: { __typename?: 'ConfigHasura', adminSecret: string, settings?: { __typename?: 'ConfigHasuraSettings', enableConsole?: boolean | null } | null }, ai?: { __typename?: 'ConfigAI', version?: string | null } | null } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, domain: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, price: number, isFree: boolean, featureMaxDbSize: number }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type GithubRepositoryFragment = { __typename?: 'githubRepositories', id: any, name: string, fullName: string, private: boolean, githubAppInstallation: { __typename?: 'githubAppInstallations', id: any, accountLogin?: string | null, accountType?: string | null, accountAvatarUrl?: string | null } };
@@ -22772,7 +22933,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?: 'ConfigRunServiceResourcesCompute', 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 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 GetRunServicesQueryVariables = Exact<{
appID: Scalars['uuid'];
@@ -22782,7 +22943,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?: 'ConfigRunServiceResourcesCompute', 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 } | 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 }> | null } | null }>, runServices_aggregate: { __typename?: 'run_service_aggregate', aggregate?: { __typename?: 'run_service_aggregate_fields', count: number } | null } } | null };
export type InsertRunServiceMutationVariables = Exact<{
object: Run_Service_Insert_Input;
@@ -23092,6 +23253,9 @@ export const ProjectFragmentDoc = gql`
enableConsole
}
}
ai {
version
}
}
featureFlags {
description
@@ -23348,6 +23512,60 @@ export function useDeletePersonalAccessTokenMutation(baseOptions?: Apollo.Mutati
export type DeletePersonalAccessTokenMutationHookResult = ReturnType<typeof useDeletePersonalAccessTokenMutation>;
export type DeletePersonalAccessTokenMutationResult = Apollo.MutationResult<DeletePersonalAccessTokenMutation>;
export type DeletePersonalAccessTokenMutationOptions = Apollo.BaseMutationOptions<DeletePersonalAccessTokenMutation, DeletePersonalAccessTokenMutationVariables>;
export const GetAiSettingsDocument = gql`
query GetAISettings($appId: uuid!) {
config(appID: $appId, resolve: false) {
ai {
version
webhookSecret
autoEmbeddings {
synchPeriodMinutes
}
openai {
apiKey
organization
}
resources {
compute {
cpu
memory
}
}
}
}
}
`;
/**
* __useGetAiSettingsQuery__
*
* To run a query within a React component, call `useGetAiSettingsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetAiSettingsQuery` 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 } = useGetAiSettingsQuery({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useGetAiSettingsQuery(baseOptions: Apollo.QueryHookOptions<GetAiSettingsQuery, GetAiSettingsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetAiSettingsQuery, GetAiSettingsQueryVariables>(GetAiSettingsDocument, options);
}
export function useGetAiSettingsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAiSettingsQuery, GetAiSettingsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetAiSettingsQuery, GetAiSettingsQueryVariables>(GetAiSettingsDocument, options);
}
export type GetAiSettingsQueryHookResult = ReturnType<typeof useGetAiSettingsQuery>;
export type GetAiSettingsLazyQueryHookResult = ReturnType<typeof useGetAiSettingsLazyQuery>;
export type GetAiSettingsQueryResult = Apollo.QueryResult<GetAiSettingsQuery, GetAiSettingsQueryVariables>;
export function refetchGetAiSettingsQuery(variables: GetAiSettingsQueryVariables) {
return { query: GetAiSettingsDocument, variables: variables }
}
export const GetAuthenticationSettingsDocument = gql`
query GetAuthenticationSettings($appId: uuid!) {
config(appID: $appId, resolve: false) {
@@ -24817,6 +25035,23 @@ export const UpdateConfigDocument = gql`
}
}
}
ai {
version
webhookSecret
autoEmbeddings {
synchPeriodMinutes
}
openai {
organization
apiKey
}
resources {
compute {
cpu
memory
}
}
}
}
}
`;

View File

@@ -83,7 +83,7 @@ export const AUTH_GRAVATAR_DEFAULT = [
/**
* Default Gravatar Rating for newly signed up users.
* Gravatar allows users to self-rate their images so that they can indicate if an image is appropriate for a certain audience.
* @see https://en.gravatar.com/site/implement/images/
* @see {@link: https://en.gravatar.com/site/implement/images/}
*/
export const AUTH_GRAVATAR_RATING = [
{

View File

@@ -5,16 +5,6 @@ export function isPlatform() {
return process.env.NEXT_PUBLIC_NHOST_PLATFORM === 'true';
}
/**
* Backend URL for the locally running instance. This is only used when running
* the Nhost Dashboard locally.
*/
export function getLocalBackendUrl() {
return `http://localhost:${
process.env.NEXT_PUBLIC_NHOST_LOCAL_SERVICES_PORT || '1337'
}`;
}
/**
* Admin secret for Hasura.
*/

View File

@@ -72,3 +72,39 @@ export function getRelativeDateByApplicationState(date: string) {
return Math.floor(difference / 1000);
}
/**
* Creates a type where all properties and nested properties are marked as required deeply, including arrays.
* @template T The type to make all properties required.
*/
export type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object
? T[K] extends Array<infer U>
? Array<DeepRequired<U>>
: DeepRequired<T[K]>
: T[K];
};
/**
* Recursively removes the property '__typename' from a JavaScript object and its nested objects and arrays.
*/
export const removeTypename = (obj: any) => {
if (!obj || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => removeTypename(item));
}
const newObj = { ...obj };
const keys = Object.keys(newObj);
keys.forEach((key) => {
if (key === '__typename') {
delete newObj[key];
} else {
newObj[key] = removeTypename(newObj[key]);
}
});
return newObj;
};

View File

@@ -9,16 +9,11 @@ import { NhostClient } from '@nhost/nextjs';
// eslint-disable-next-line no-nested-ternary
const nhost = isPlatform()
? new NhostClient({ backendUrl: process.env.NEXT_PUBLIC_NHOST_BACKEND_URL })
: getAuthServiceUrl() &&
getGraphqlServiceUrl() &&
getStorageServiceUrl() &&
getFunctionsServiceUrl()
? new NhostClient({
authUrl: getAuthServiceUrl(),
graphqlUrl: getGraphqlServiceUrl(),
storageUrl: getStorageServiceUrl(),
functionsUrl: getFunctionsServiceUrl(),
storageUrl: getStorageServiceUrl(),
})
: new NhostClient({ subdomain: 'local' });

View File

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

View File

@@ -0,0 +1,7 @@
import { recoilPersist } from 'recoil-persist';
const { persistAtom } = recoilPersist({
key: 'devAssistant',
});
export default persistAtom;

View File

@@ -1,4 +1,5 @@
const defaultTheme = require('tailwindcss/defaultTheme');
const plugin = require('tailwindcss/plugin');
/** @type {import('tailwindcss').Config} */
module.exports = {
@@ -150,11 +151,34 @@ module.exports = {
'inter-var': ['Inter var', ...defaultTheme.fontFamily.sans],
mono: ['"Roboto Mono"', ...defaultTheme.fontFamily.mono],
},
keyframes: {
blinking: {
'0%': { opacity: 0 },
'50%': { opacity: 1 },
'100%': { opacity: 0 },
},
},
animation: {
blinking: 'blinking 1s infinite',
},
},
},
variants: {
extend: {},
},
// eslint-disable-next-line global-require
plugins: [require('@tailwindcss/forms')],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
plugin(({ matchUtilities, theme }) => {
matchUtilities(
{
'animate-delay': (value) => ({
animationDelay: value,
}),
},
{ values: theme('transitionDelay') },
);
}),
],
};

View File

@@ -1,5 +1,11 @@
# @nhost/docs
## 0.7.4
### Patch Changes
- 2a04bc9e5: added functions to custom domains documentation
## 0.7.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.7.3",
"version": "0.7.4",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/docker-compose
## 0.0.6
### Patch Changes
- a1c5c97a5: Clarify instructions for running the Nhost dashboard with Docker Compose
## 0.0.5
### Patch Changes

View File

@@ -25,10 +25,12 @@ The following endpoints are now exposed:
## Running the Nhost dashboard locally
In order to use the Nhost dashboard, you need to run the [Hasura console locally from the Hasura CLI](https://hasura.io/docs/latest/hasura-cli/commands/hasura_console/):
In order for you to be able to make edits to the database from the Nhost dashboard, you need to run the [Hasura console locally from the Hasura CLI](https://hasura.io/docs/latest/hasura-cli/commands/hasura_console/):
```sh
hasura console
```
The Nhost Dashboard also requires the Hasura admin secret to `nhost-admin-secret`. This will change in the future. If you can't wait, don't hesitate to contribute.
The Nhost Dashboard [uses](https://github.com/nhost/nhost/discussions/2398) the [Hasura migrations API](https://hasura.io/docs/latest/hasura-cli/commands/hasura_console/#options) in order to make edits to the database. It runs over port 9693 and is only accessible through running the Hasura console from the CLI. Because the Docker compose still only uses the graphql-engine Hasura Docker image and does not include the CLI image, that is why you need to run it locally. See https://github.com/nhost/nhost/issues/1220. Users are welcome to contibute a Docker compose that includes the CLI image to resolve this.
The Nhost Dashboard also requires the Hasura admin secret to `nhost-admin-secret` specified in the `.env` file.

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost-examples/docker-compose",
"version": "0.0.5",
"version": "0.0.6",
"private": true,
"scripts": {
"e2e": "vitest run"

View File

@@ -1,5 +1,16 @@
# @nhost-examples/multi-tenant-one-to-many
## 2.0.0
### Major Changes
- bc9eff6e4: chore: remove support for using backendUrl when instantiating the Nhost client
### Patch Changes
- Updated dependencies [bc9eff6e4]
- @nhost/nhost-js@3.0.0
## 1.0.4
### Patch Changes

View File

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

View File

@@ -1,7 +1,7 @@
import { NhostClient } from "@nhost/nhost-js";
import { NhostClient } from '@nhost/nhost-js'
const nhost = new NhostClient({
backendUrl: "http://localhost:1337",
});
subdomain: 'local'
})
export { nhost };
export { nhost }

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