Compare commits
25 Commits
@nhost/rea
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
037bd74764 | ||
|
|
0f6ce52c4e | ||
|
|
6696172bcb | ||
|
|
b0e848d353 | ||
|
|
cea3ef5c8a | ||
|
|
a05db74bb6 | ||
|
|
73f3d69776 | ||
|
|
a99f034bd4 | ||
|
|
3b37af06a0 | ||
|
|
86ecf27b23 | ||
|
|
1b5dc5e7f5 | ||
|
|
21708be3d2 | ||
|
|
f16e2305c3 | ||
|
|
5d6c349350 | ||
|
|
245a1b44c4 | ||
|
|
ca75f731af | ||
|
|
c48be24d13 | ||
|
|
60b5bf20d7 | ||
|
|
8f94bc6332 | ||
|
|
75c73c4884 | ||
|
|
4c6a6bb6c1 | ||
|
|
60b685ab02 | ||
|
|
2e65bc6dc0 | ||
|
|
14e6100722 | ||
|
|
479dba102e |
@@ -14,7 +14,7 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8.10.5
|
||||
version: 9.15.0
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -26,10 +26,10 @@ runs:
|
||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: ${{ runner.os }}-node-
|
||||
- name: Use Node.js v18
|
||||
- name: Use Node.js v20
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- shell: bash
|
||||
name: Install packages
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
26
.github/workflows/changesets.yaml
vendored
26
.github/workflows/changesets.yaml
vendored
@@ -65,29 +65,13 @@ jobs:
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
uses: ./.github/workflows/deploy-dashboard.yaml
|
||||
with:
|
||||
git_ref: ${{ github.ref_name }}
|
||||
environment: production
|
||||
secrets: inherit
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
|
||||
1
.github/workflows/ci.yaml
vendored
1
.github/workflows/ci.yaml
vendored
@@ -18,7 +18,6 @@ env:
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
|
||||
|
||||
1
.github/workflows/dashboard.yaml
vendored
1
.github/workflows/dashboard.yaml
vendored
@@ -8,7 +8,6 @@ env:
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
58
.github/workflows/deploy-dashboard.yaml
vendored
Normal file
58
.github/workflows/deploy-dashboard.yaml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: 'dashboard: release form'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
git_ref:
|
||||
type: string
|
||||
description: 'Branch, tag, or commit SHA'
|
||||
required: true
|
||||
|
||||
environment:
|
||||
type: choice
|
||||
description: 'Deployment environment'
|
||||
required: true
|
||||
default: staging
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
git_ref:
|
||||
required: true
|
||||
type: string
|
||||
environment:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.git_ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
|
||||
- name: Trigger Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ inputs.environment == 'production' && secrets.DASHBOARD_VERCEL_PROJECT_ID || secrets.DASHBOARD_STAGING_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
echo "Deploying to: ${{ inputs.environment }}..."
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
1
.github/workflows/gen_ai_review.yaml
vendored
1
.github/workflows/gen_ai_review.yaml
vendored
@@ -12,7 +12,6 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
<a href="https://twitter.com/nhost">Twitter</a>
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/discord">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://gurubase.io/g/nhost">Ask Nhost Guru (third party, unofficial)</a>
|
||||
<br />
|
||||
|
||||
<hr />
|
||||
@@ -148,4 +150,4 @@ Here are some ways of contributing to making Nhost better:
|
||||
<p align="center">
|
||||
<img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
</a>
|
||||
@@ -42,7 +42,6 @@ module.exports = {
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: 'http://localhost:1337',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 2.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 0f6ce52: fix: consolidate useProject hook and fix jwt expired error
|
||||
|
||||
## 2.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- cea3ef5: Feat: add org and project placeholders
|
||||
|
||||
## 2.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 86ecf27: feat: add support for additional metrics in overview
|
||||
- 21708be: feat: dashboard: add support for storage buckets to AI assistants
|
||||
|
||||
## 1.30.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -30,7 +30,7 @@ ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRA
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_CONFIGSERVER_URL __NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__
|
||||
|
||||
RUN yarn global add pnpm@8.10.5
|
||||
RUN yarn global add pnpm@9.15.0
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
|
||||
@@ -100,7 +100,6 @@ pnpm storybook --port 6007
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "2.7.1",
|
||||
"version": "2.11.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
137
dashboard/src/components/common/SelectOrg/SelectOrg.tsx
Normal file
137
dashboard/src/components/common/SelectOrg/SelectOrg.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { } from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export default function SelectOrganizationAndProject() {
|
||||
const { orgs, loading } = useOrgs();
|
||||
const router = useRouter();
|
||||
|
||||
const organizations = orgs.map((org) => ({
|
||||
name: org.name,
|
||||
value: `/orgs/${org.slug}`,
|
||||
}));
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const goToOrgPage = async (org: {
|
||||
name: string;
|
||||
value: string;
|
||||
}) => {
|
||||
const { slug } = router.query;
|
||||
await router.push({
|
||||
pathname: `${org.value}/${
|
||||
(() => {
|
||||
if (!slug) {
|
||||
return '';
|
||||
}
|
||||
return Array.isArray(slug) ? slug.join('/') : slug;
|
||||
})()
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
const orgsToDisplay = filter
|
||||
? organizations.filter((org) =>
|
||||
org.name.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: organizations;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading organizations..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start w-full h-full px-5 py-4 mx-auto bg-background">
|
||||
<div className="mx-auto flex h-full w-full max-w-[760px] flex-col gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
Select an Organization
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{orgsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{orgsToDisplay.map((org, index) => (
|
||||
<Fragment key={org.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToOrgPage(org)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={org.name}
|
||||
secondary={`${org.name} / ${org.name}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < orgs.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/common/SelectOrg/index.ts
Normal file
1
dashboard/src/components/common/SelectOrg/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as SelectOrg } from './SelectOrg';
|
||||
@@ -0,0 +1,141 @@
|
||||
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { List } from '@/components/ui/v2/List';
|
||||
import { ListItem } from '@/components/ui/v2/ListItem';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { } from '@/utils/__generated__/graphql';
|
||||
import { Divider } from '@mui/material';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export default function SelectOrganizationAndProject() {
|
||||
const { orgs, loading } = useOrgs();
|
||||
const router = useRouter();
|
||||
|
||||
const projects = orgs.flatMap((org) =>
|
||||
org.apps.map((app) => ({
|
||||
organizationName: org.name,
|
||||
projectName: app.name,
|
||||
value: `/orgs/${org.slug}/projects/${app.subdomain}`,
|
||||
})),
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const handleFilterChange = useMemo(
|
||||
() =>
|
||||
debounce((event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
|
||||
|
||||
const goToProjectPage = async (project: {
|
||||
organizationName: string;
|
||||
projectName: string;
|
||||
value: string;
|
||||
}) => {
|
||||
const { slug } = router.query;
|
||||
await router.push({
|
||||
pathname: `${project.value}/${
|
||||
(() => {
|
||||
if (!slug) {
|
||||
return '';
|
||||
}
|
||||
return Array.isArray(slug) ? slug.join('/') : slug;
|
||||
})()
|
||||
}`,
|
||||
});
|
||||
};
|
||||
|
||||
const projectsToDisplay = filter
|
||||
? projects.filter((project) =>
|
||||
project.projectName.toLowerCase().includes(filter.toLowerCase()),
|
||||
)
|
||||
: projects;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex w-full justify-center">
|
||||
<ActivityIndicator
|
||||
delay={500}
|
||||
label="Loading organizations and projects..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start w-full h-full px-5 py-4 mx-auto bg-background">
|
||||
<div className="mx-auto flex h-full w-full max-w-[760px] flex-col gap-4 py-6 sm:py-14">
|
||||
<Text variant="h2" component="h1" className="">
|
||||
Select a Project
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<div className="mb-2 flex w-full">
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
onChange={handleFilterChange}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
{projectsToDisplay.length === 0 ? (
|
||||
<Box className="h-import py-2">
|
||||
<Text variant="subtitle2">No results found.</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<List className="h-import overflow-y-auto">
|
||||
{projectsToDisplay.map((project, index) => (
|
||||
<Fragment key={project.value}>
|
||||
<ListItem.Root
|
||||
className="grid grid-flow-col justify-start gap-2 py-2.5"
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="primary"
|
||||
onClick={() => goToProjectPage(project)}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<ListItem.Avatar>
|
||||
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
</ListItem.Avatar>
|
||||
<ListItem.Text
|
||||
primary={project.projectName}
|
||||
secondary={`${project.organizationName} / ${project.projectName}`}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
{index < projects.length - 1 && <Divider component="li" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
export { default as SelectOrgAndProject } from './SelectOrgAndProject';
|
||||
@@ -20,8 +20,7 @@ interface AINavLinkProps extends ListItemButtonProps {
|
||||
*/
|
||||
href: string;
|
||||
/**
|
||||
* Determines whether or not the link should be active if it's href exactly
|
||||
* matches the current route.
|
||||
* Determines whether or not the link should be active if href matches the current route.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
@@ -87,7 +86,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<>
|
||||
<Backdrop
|
||||
open={expanded}
|
||||
className="absolute top-0 left-0 bottom-0 right-0 z-[34] md:hidden"
|
||||
className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => setExpanded(false)}
|
||||
@@ -104,7 +103,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<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',
|
||||
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
|
||||
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||
className,
|
||||
)}
|
||||
@@ -119,6 +118,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
>
|
||||
Auto-Embeddings
|
||||
</AINavLink>
|
||||
|
||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||
Assistants
|
||||
</AINavLink>
|
||||
|
||||
@@ -8,20 +8,15 @@ import {
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from '@/components/ui/v3/command';
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/v3/hover-card';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/v3/popover';
|
||||
import { ProjectStatusIndicator } from '@/features/orgs/components/common/ProjectStatusIndicator';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -31,56 +26,6 @@ type Option = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
function ProjectStatusIndicator({ status }: { status: ApplicationStatus }) {
|
||||
const indicatorStyles: Record<
|
||||
number,
|
||||
{ className: string; description: string }
|
||||
> = {
|
||||
[ApplicationStatus.Errored]: {
|
||||
className: 'bg-destructive',
|
||||
description: 'Project errored',
|
||||
},
|
||||
[ApplicationStatus.Pausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is pausing',
|
||||
},
|
||||
[ApplicationStatus.Restoring]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is restoring',
|
||||
},
|
||||
[ApplicationStatus.Paused]: {
|
||||
className: 'bg-slate-400',
|
||||
description: 'Project is paused',
|
||||
},
|
||||
[ApplicationStatus.Unpausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is unpausing',
|
||||
},
|
||||
[ApplicationStatus.Live]: {
|
||||
className: 'bg-primary-main',
|
||||
description: 'Project is live',
|
||||
},
|
||||
};
|
||||
const style = indicatorStyles[status];
|
||||
|
||||
if (style) {
|
||||
return (
|
||||
<HoverCard openDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span
|
||||
className={cn('mt-[1px] h-2 w-2 rounded-full', style.className)}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="top" className="h-fit w-fit py-2">
|
||||
{style.description}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function ProjectsComboBox() {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { IconProps } from '@/components/ui/v2/icons';
|
||||
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
|
||||
|
||||
function FileStoresIcon(props: IconProps) {
|
||||
return (
|
||||
<SvgIcon
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
aria-label="FileStores Icon"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 22v-9" />
|
||||
<path d="M15.17 2.21a1.67 1.67 0 0 1 1.63 0L21 4.57a1.93 1.93 0 0 1 0 3.36L8.82 14.79a1.655 1.655 0 0 1-1.64 0L3 12.43a1.93 1.93 0 0 1 0-3.36z" />
|
||||
<path d="M20 13v3.87a2.06 2.06 0 0 1-1.11 1.83l-6 3.08a1.93 1.93 0 0 1-1.78 0l-6-3.08A2.06 2.06 0 0 1 4 16.87V13" />
|
||||
<path d="M21 12.43a1.93 1.93 0 0 0 0-3.36L8.83 2.2a1.64 1.64 0 0 0-1.63 0L3 4.57a1.93 1.93 0 0 0 0 3.36l12.18 6.86a1.636 1.636 0 0 0 1.63 0z" />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
FileStoresIcon.displayName = 'FileStoresIcon';
|
||||
|
||||
export default FileStoresIcon;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoresIcon } from './FileStoresIcon';
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -10,14 +11,14 @@ 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 { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient'
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertAssistantMutation,
|
||||
useUpdateAssistantMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -28,6 +29,7 @@ export const validationSchema = Yup.object({
|
||||
description: Yup.string(),
|
||||
instructions: Yup.string().required('The instructions are required'),
|
||||
model: Yup.string().required('The model is required'),
|
||||
fileStore: Yup.string().label('File Store'),
|
||||
graphql: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
@@ -64,14 +66,14 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
* To use in conjunction with initialData to allow for updating the Assistant Configuration
|
||||
*/
|
||||
assistantId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues;
|
||||
initialData?: AssistantFormValues
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
@@ -114,26 +116,26 @@ export default function AssistantForm({
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
const createOrUpdateAssistant = async (
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
// remove any __typename from the form values
|
||||
const payload = removeTypename(values);
|
||||
|
||||
if (values.webhooks.length === 0) {
|
||||
if (values.webhooks?.length === 0) {
|
||||
delete payload.webhooks;
|
||||
}
|
||||
|
||||
if (values.graphql.length === 0) {
|
||||
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
|
||||
@@ -158,11 +160,13 @@ export default function AssistantForm({
|
||||
};
|
||||
|
||||
const handleSubmit = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateAutoEmbeddings(values);
|
||||
await createOrUpdateAssistant(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
@@ -282,6 +286,7 @@ export default function AssistantForm({
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<GraphqlDataSourcesFormSection />
|
||||
<WebhooksDataSourcesFormSection />
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,12 @@ import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
|
||||
|
||||
interface AssistantsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
* The list of assistants.
|
||||
*/
|
||||
assistants: Assistant[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
* Function to be called after a submitting the form for either creating or updating an assistant.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
|
||||
@@ -14,9 +14,6 @@ export default function Estimate() {
|
||||
|
||||
const amountDue = useMemo(() => {
|
||||
const amount = data?.billingGetNextInvoice?.AmountDue;
|
||||
if (!amount) {
|
||||
return 'N/A';
|
||||
}
|
||||
if (typeof amount !== 'number') {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/v3/hover-card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
|
||||
export default function ProjectStatusIndicator({
|
||||
status,
|
||||
}: {
|
||||
status: ApplicationStatus;
|
||||
}) {
|
||||
const indicatorStyles: Record<
|
||||
number,
|
||||
{ className: string; description: string }
|
||||
> = {
|
||||
[ApplicationStatus.Errored]: {
|
||||
className: 'bg-destructive',
|
||||
description: 'Project errored',
|
||||
},
|
||||
[ApplicationStatus.Pausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is pausing',
|
||||
},
|
||||
[ApplicationStatus.Restoring]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is restoring',
|
||||
},
|
||||
[ApplicationStatus.Paused]: {
|
||||
className: 'bg-slate-400',
|
||||
description: 'Project is paused',
|
||||
},
|
||||
[ApplicationStatus.Unpausing]: {
|
||||
className: 'bg-primary-main animate-blinking',
|
||||
description: 'Project is unpausing',
|
||||
},
|
||||
[ApplicationStatus.Live]: {
|
||||
className: 'bg-primary-main',
|
||||
description: 'Project is live',
|
||||
},
|
||||
};
|
||||
const style = indicatorStyles[status];
|
||||
|
||||
if (style) {
|
||||
return (
|
||||
<HoverCard openDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
'mt-[2px] h-2 w-2 flex-shrink-0 rounded-full',
|
||||
style.className,
|
||||
)}
|
||||
/>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="top" className="h-fit w-fit py-2">
|
||||
{style.description}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ProjectStatusIndicator } from './ProjectStatusIndicator';
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { ProjectStatusIndicator } from '@/features/orgs/components/common/ProjectStatusIndicator';
|
||||
import { DeploymentStatusMessage } from '@/features/orgs/projects/deployments/components/DeploymentStatusMessage';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { DeploymentStatusMessage } from '@/features/projects/deployments/components/DeploymentStatusMessage';
|
||||
import {
|
||||
useGetProjectsQuery,
|
||||
type GetProjectsQuery,
|
||||
@@ -22,20 +23,21 @@ function ProjectCard({ project }: { project: Project }) {
|
||||
return (
|
||||
<Link
|
||||
href={`/orgs/${org?.slug}/projects/${project.subdomain}`}
|
||||
className="flex cursor-pointer flex-col gap-4 rounded-lg border bg-background p-4 hover:shadow-sm"
|
||||
className="flex h-44 cursor-pointer flex-col gap-4 rounded-lg border bg-background p-4 hover:shadow-sm"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="flex w-full flex-row items-center space-x-2">
|
||||
<Box className="h-6 w-6 flex-shrink-0" />
|
||||
<p className="truncate text-lg font-bold">{project.name}</p>
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<Box className="mt-[2px] h-5 w-5 flex-shrink-0" />
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="truncate font-bold">{project.name}</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{project.region.name}
|
||||
</span>
|
||||
</div>
|
||||
<ProjectStatusIndicator status={project.appStates[0].stateId} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-start gap-2">
|
||||
<DeploymentStatusMessage
|
||||
appCreatedAt={project.createdAt}
|
||||
deployment={latestDeployment}
|
||||
/>
|
||||
<div className="flex flex-1 flex-row items-start gap-2">
|
||||
<DeploymentStatusMessage deployment={latestDeployment} />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-end">
|
||||
@@ -53,6 +55,7 @@ export default function ProjectsGrid() {
|
||||
orgSlug: org?.slug,
|
||||
},
|
||||
skip: !org,
|
||||
pollInterval: 10 * 1000,
|
||||
});
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
@@ -100,7 +103,7 @@ export default function ProjectsGrid() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
<div className="grid grid-cols-1 gap-4 p-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{filteredProjects.map((project) => (
|
||||
<ProjectCard key={project.id} project={project} />
|
||||
))}
|
||||
|
||||
@@ -128,6 +128,9 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||
Assistants
|
||||
</AINavLink>
|
||||
<AINavLink href="/file-stores" exact={false} onClick={handleSelect}>
|
||||
File Stores
|
||||
</AINavLink>
|
||||
</List>
|
||||
</nav>
|
||||
</Box>
|
||||
|
||||
@@ -14,21 +14,27 @@ import { WebhooksDataSourcesFormSection } from '@/features/orgs/projects/ai/Assi
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import {
|
||||
useInsertAssistantMutation,
|
||||
useUpdateAssistantMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import { ControlledSelect } from '@/components/form/ControlledSelect';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
|
||||
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'),
|
||||
fileStore: Yup.string().label('File Store'),
|
||||
graphql: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(),
|
||||
@@ -65,14 +71,17 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
* To use in conjunction with initialData to allow for updating the Assistant Configuration
|
||||
*/
|
||||
assistantId?: string;
|
||||
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues;
|
||||
initialData?: AssistantFormValues & {
|
||||
fileStores?: string[];
|
||||
};
|
||||
fileStores?: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
@@ -87,6 +96,7 @@ export interface AssistantFormProps extends DialogFormProps {
|
||||
export default function AssistantForm({
|
||||
assistantId,
|
||||
initialData,
|
||||
fileStores,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
@@ -103,8 +113,27 @@ export default function AssistantForm({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const isFileStoreSupported = useIsFileStoreSupported();
|
||||
|
||||
const fileStoresOptions = fileStores
|
||||
? fileStores.map((fileStore: GraphiteFileStore) => ({
|
||||
label: fileStore.name,
|
||||
value: fileStore.name,
|
||||
id: fileStore.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const assistantFileStore = initialData?.fileStores
|
||||
? fileStores?.find((fileStore: GraphiteFileStore) =>
|
||||
fileStore.id === initialData?.fileStores[0]
|
||||
)
|
||||
: null;
|
||||
|
||||
const formDefaultValues = { ...initialData, fileStores: [] };
|
||||
formDefaultValues.fileStore = assistantFileStore ? assistantFileStore.id : '';
|
||||
|
||||
const form = useForm<AssistantFormValues>({
|
||||
defaultValues: initialData,
|
||||
defaultValues: formDefaultValues,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -120,22 +149,32 @@ export default function AssistantForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const createOrUpdateAutoEmbeddings = async (
|
||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
||||
const createOrUpdateAssistant = async (
|
||||
values: DeepRequired<AssistantFormValues> & {
|
||||
assistantID: string;
|
||||
},
|
||||
) => {
|
||||
// remove any __typename from the form values
|
||||
const payload = removeTypename(values);
|
||||
|
||||
if (values.webhooks.length === 0) {
|
||||
if (values.webhooks?.length === 0) {
|
||||
delete payload.webhooks;
|
||||
}
|
||||
|
||||
if (values.graphql.length === 0) {
|
||||
if (values.graphql?.length === 0) {
|
||||
delete payload.graphql;
|
||||
}
|
||||
|
||||
if (isFileStoreSupported && values.fileStore) {
|
||||
payload.fileStores = [values.fileStore];
|
||||
}
|
||||
if (!isFileStoreSupported) {
|
||||
delete payload.fileStores;
|
||||
}
|
||||
|
||||
// remove assistantId because the update mutation fails otherwise
|
||||
delete payload.assistantID;
|
||||
delete payload.fileStore;
|
||||
|
||||
// If the assistantId is set then we do an update
|
||||
if (assistantId) {
|
||||
@@ -152,7 +191,7 @@ export default function AssistantForm({
|
||||
await insertAssistantMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...values,
|
||||
...payload,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -163,7 +202,7 @@ export default function AssistantForm({
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateAutoEmbeddings(values);
|
||||
await createOrUpdateAssistant(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
@@ -175,6 +214,10 @@ export default function AssistantForm({
|
||||
);
|
||||
};
|
||||
|
||||
const fileStoreTooltip = isFileStoreSupported
|
||||
? "If specified, all text documents in this file store will be available to the assistant."
|
||||
: "Please upgrade Graphite to its latest version in order to use file stores.";
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
@@ -285,6 +328,36 @@ export default function AssistantForm({
|
||||
/>
|
||||
<GraphqlDataSourcesFormSection />
|
||||
<WebhooksDataSourcesFormSection />
|
||||
<ControlledSelect
|
||||
slotProps={{
|
||||
popper: { disablePortal: false, className: 'z-[10000]' },
|
||||
}}
|
||||
id="fileStore"
|
||||
name="fileStore"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>File Store</Text>
|
||||
<Tooltip title={fileStoreTooltip}>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
fullWidth
|
||||
error={!!errors?.model?.message}
|
||||
helperText={errors?.model?.message}
|
||||
disabled={!isFileStoreSupported}
|
||||
>
|
||||
<Option value="" />
|
||||
{fileStoresOptions.map((fileStore) => (
|
||||
<Option key={fileStore.id} value={fileStore.id}>
|
||||
{fileStore.label}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
</div>
|
||||
|
||||
<Box className="flex flex-row justify-between w-full p-4 border-t rounded">
|
||||
|
||||
@@ -11,16 +11,22 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import { DeleteAssistantModal } from '@/features/orgs/projects/ai/DeleteAssistantModal';
|
||||
import { type Assistant } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/assistants';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
interface AssistantsListProps {
|
||||
/**
|
||||
* The run services fetched from entering the users page.
|
||||
* The list of assistants
|
||||
*/
|
||||
assistants: Assistant[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a service.
|
||||
* The list of file stores
|
||||
*/
|
||||
fileStores: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating an assistant.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
@@ -35,6 +41,7 @@ interface AssistantsListProps {
|
||||
|
||||
export default function AssistantsList({
|
||||
assistants,
|
||||
fileStores,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: AssistantsListProps) {
|
||||
@@ -49,6 +56,7 @@ export default function AssistantsList({
|
||||
initialData={{
|
||||
...assistant,
|
||||
}}
|
||||
fileStores={fileStores}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
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 { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { useDeleteFileStoreMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DeleteFileStoreModalProps {
|
||||
fileStore: GraphiteFileStore;
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export default function DeleteFileStoreModal({
|
||||
fileStore,
|
||||
onDelete,
|
||||
close,
|
||||
}: DeleteFileStoreModalProps) {
|
||||
const [remove, setRemove] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
|
||||
const [deleteFileStoreMutation] = useDeleteFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const deleteFileStore = async () => {
|
||||
await deleteFileStoreMutation({
|
||||
variables: {
|
||||
id: fileStore.id,
|
||||
},
|
||||
});
|
||||
await onDelete?.();
|
||||
close();
|
||||
};
|
||||
|
||||
async function handleClick() {
|
||||
setLoading(true);
|
||||
|
||||
await execPromiseWithErrorToast(deleteFileStore, {
|
||||
loadingMessage: 'Deleting the file store...',
|
||||
successMessage: 'The file store has been deleted successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while deleting the file store. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
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 File Store {fileStore?.name}{' '}
|
||||
</Text>{' '}
|
||||
<Text variant="subtitle2">
|
||||
{' '}
|
||||
Are you sure you want to delete this File Store?{' '}
|
||||
</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 ${fileStore?.name}`}
|
||||
className="py-2"
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete File Store"
|
||||
/>
|
||||
</Box>
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button
|
||||
color="error"
|
||||
onClick={handleClick}
|
||||
disabled={!remove}
|
||||
loading={loading}
|
||||
>
|
||||
Delete File Store
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteFileStoreModal } from './DeleteFileStoreModal';
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
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 { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient'
|
||||
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import {
|
||||
useInsertFileStoreMutation,
|
||||
useUpdateFileStoreMutation,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useGetBucketsQuery } from '@/utils/__generated__/graphql';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
name: Yup.string().required('The name is required'),
|
||||
buckets: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
label: Yup.string(),
|
||||
value: Yup.string(),
|
||||
}),
|
||||
)
|
||||
.label('Buckets')
|
||||
.required('At least one bucket is required'),
|
||||
});
|
||||
|
||||
export type FileStoreFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export interface FileStoreFormProps extends DialogFormProps {
|
||||
id?: string;
|
||||
initialData?: Omit<FileStoreFormValues, 'buckets'> & { buckets: string[] };
|
||||
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export default function FileStoreForm({
|
||||
id,
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
}: FileStoreFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
|
||||
const [insertFileStore] = useInsertFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const [updateFileStore] = useUpdateFileStoreMutation({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
|
||||
const { data: buckets } = useGetBucketsQuery({
|
||||
client: remoteProjectGQLClient,
|
||||
});
|
||||
|
||||
const bucketOptions = buckets
|
||||
? buckets.buckets.map((bucket) => ({
|
||||
label: bucket.id,
|
||||
value: bucket.id,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const formDefaultValues = { ...initialData, buckets: [] };
|
||||
formDefaultValues.buckets = initialData?.buckets
|
||||
? initialData.buckets.map((bucket) => ({
|
||||
label: bucket,
|
||||
value: bucket,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const form = useForm<FileStoreFormValues>({
|
||||
defaultValues: formDefaultValues,
|
||||
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 createOrUpdateFileStore = async (
|
||||
values: DeepRequired<FileStoreFormValues> & { id: string },
|
||||
) => {
|
||||
const payload = removeTypename(values);
|
||||
delete payload.id;
|
||||
delete payload.vectorStoreID;
|
||||
|
||||
if (id) {
|
||||
await updateFileStore({
|
||||
variables: {
|
||||
id,
|
||||
object: { ...payload, buckets: values.buckets.map((b) => b.value) },
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await insertFileStore({
|
||||
variables: {
|
||||
object: { ...values, buckets: values.buckets.map((b) => b.value) },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (
|
||||
values: DeepRequired<FileStoreFormValues> & { id: string },
|
||||
) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await createOrUpdateFileStore(values);
|
||||
onSubmit?.();
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Creating File Store...',
|
||||
successMessage: 'The File Store has been created successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while creating the File Store. Please try again.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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 file store">
|
||||
<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
|
||||
/>
|
||||
|
||||
<ControlledAutocomplete
|
||||
id="buckets"
|
||||
name="buckets"
|
||||
label={
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text>Buckets</Text>
|
||||
<Tooltip title="One or more buckets from storage from which documents can be used by Assistants">
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
className="h-4 w-4"
|
||||
color="primary"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
fullWidth
|
||||
multiple
|
||||
aria-label="Buckets"
|
||||
error={!!errors.buckets}
|
||||
options={bucketOptions}
|
||||
helperText={errors?.buckets?.message}
|
||||
/>
|
||||
</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={id ? <ArrowsClockwise /> : <PlusIcon />}
|
||||
>
|
||||
{id ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoreForm } from './FileStoreForm';
|
||||
@@ -0,0 +1,157 @@
|
||||
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 { FileStoresIcon } from '@/components/ui/v2/icons/FileStoresIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { DeleteFileStoreModal } from '@/features/orgs/projects/ai/DeleteFileStoreModal';
|
||||
import { FileStoreForm } from '@/features/orgs/projects/ai/FileStoreForm';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
import { copy } from '@/utils/copy';
|
||||
|
||||
interface FileStoresListProps {
|
||||
/**
|
||||
* List of File Stores to be displayed.
|
||||
*/
|
||||
fileStores: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating a File Store.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
*
|
||||
*/
|
||||
onDelete?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function FileStoresList({
|
||||
fileStores,
|
||||
onCreateOrUpdate,
|
||||
onDelete,
|
||||
}: FileStoresListProps) {
|
||||
const { openDrawer, openDialog, closeDialog } = useDialog();
|
||||
|
||||
const viewFileStore = async (fileStore: GraphiteFileStore) => {
|
||||
openDrawer({
|
||||
title: fileStore.name,
|
||||
component: (
|
||||
<FileStoreForm
|
||||
id={fileStore.id}
|
||||
initialData={{ ...fileStore }}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const deleteFileStore = async (fileStore: GraphiteFileStore) => {
|
||||
openDialog({
|
||||
component: (
|
||||
<DeleteFileStoreModal
|
||||
fileStore={fileStore}
|
||||
close={closeDialog}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col">
|
||||
{fileStores.map((fileStore) => (
|
||||
<Box
|
||||
key={fileStore.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={() => viewFileStore(fileStore)}
|
||||
className="flex w-full flex-row justify-between"
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-center space-x-4">
|
||||
<FileStoresIcon className="h-5 w-5" />
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
{fileStore?.name ?? 'unset'}
|
||||
</Text>
|
||||
<div className="hidden flex-row items-center space-x-2 md:flex">
|
||||
<Text variant="subtitle1" className="font-mono text-xs">
|
||||
{fileStore.id}
|
||||
</Text>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
copy(fileStore.id, 'File Store 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={() => viewFileStore(fileStore)}
|
||||
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 {fileStore?.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={() => deleteFileStore(fileStore)}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
<Text className="font-medium" color="error">
|
||||
Delete {fileStore?.name}
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as FileStoresList } from './FileStoresList';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useIsFileStoreSupported } from './useIsFileStoreSupported';
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useGetConfiguredVersionsQuery } from '@/utils/__generated__/graphql';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function compareSemver(v1: string, v2: string): number {
|
||||
const parse = (v: string) => v.split('.').map(Number);
|
||||
const [a, b] = [parse(v1), parse(v2)];
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (a[i] > b[i]) { return 1; }
|
||||
if (a[i] < b[i]) { return -1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const MIN_VERSION_WITH_FILE_STORE_SUPPORT = '0.6.2';
|
||||
|
||||
export default function useIsFileStoreSupported() {
|
||||
const [isFileStoreSupported, setIsFileStoreSupported] = useState<boolean | null>(null);
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { data, loading, error } = useGetConfiguredVersionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data?.config?.ai?.version) {
|
||||
setIsFileStoreSupported(compareSemver(data.config.ai.version, MIN_VERSION_WITH_FILE_STORE_SUPPORT) >= 0);
|
||||
}
|
||||
}, [data, loading]);
|
||||
|
||||
return {
|
||||
isFileStoreSupported,
|
||||
version: data?.config?.ai?.version,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
@@ -39,10 +39,12 @@ export default function useUpdateColumnMutation({
|
||||
const {
|
||||
query: { dataSourceSlug, schemaSlug, tableSlug },
|
||||
} = useRouter();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { project } = useProject();
|
||||
|
||||
const appUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
'hasura',
|
||||
);
|
||||
const mutationFn = isPlatform ? updateColumn : updateColumnMigration;
|
||||
@@ -55,7 +57,7 @@ export default function useUpdateColumnMutation({
|
||||
adminSecret:
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: customAdminSecret || currentProject?.config?.hasura.adminSecret,
|
||||
: customAdminSecret || project?.config?.hasura.adminSecret,
|
||||
dataSource: customDataSource || (dataSourceSlug as string),
|
||||
schema: customSchema || (schemaSlug as string),
|
||||
table: customTable || (tableSlug as string),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import type { MutationOptions } from '@tanstack/react-query';
|
||||
@@ -40,10 +40,12 @@ export default function useUpdateRecordMutation<TData extends object = {}>({
|
||||
const {
|
||||
query: { dataSourceSlug, schemaSlug, tableSlug },
|
||||
} = useRouter();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { project } = useProject();
|
||||
|
||||
const appUrl = generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
'hasura',
|
||||
);
|
||||
|
||||
@@ -55,7 +57,7 @@ export default function useUpdateRecordMutation<TData extends object = {}>({
|
||||
adminSecret:
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: customAdminSecret || currentProject?.config?.hasura.adminSecret,
|
||||
: customAdminSecret || project?.config?.hasura.adminSecret,
|
||||
dataSource: customDataSource || (dataSourceSlug as string),
|
||||
schema: customSchema || (schemaSlug as string),
|
||||
table: customTable || (tableSlug as string),
|
||||
|
||||
@@ -23,30 +23,17 @@ afterAll(() => {
|
||||
});
|
||||
|
||||
test('should render the avatar of the user who deployed the application', () => {
|
||||
render(
|
||||
<DeploymentStatusMessage
|
||||
deployment={defaultDeployment}
|
||||
appCreatedAt="2023-02-24"
|
||||
/>,
|
||||
);
|
||||
render(<DeploymentStatusMessage deployment={defaultDeployment} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('img', {
|
||||
name: `Avatar of ${defaultDeployment.commitUserName}`,
|
||||
}),
|
||||
).toHaveAttribute(
|
||||
'style',
|
||||
`background-image: url(${defaultDeployment.commitUserAvatarUrl});`,
|
||||
);
|
||||
).toHaveAttribute('src', `${defaultDeployment.commitUserAvatarUrl}`);
|
||||
});
|
||||
|
||||
test('should render "updated just now" when the deployment is in progress and has not ended', () => {
|
||||
render(
|
||||
<DeploymentStatusMessage
|
||||
deployment={defaultDeployment}
|
||||
appCreatedAt="2023-02-24"
|
||||
/>,
|
||||
);
|
||||
render(<DeploymentStatusMessage deployment={defaultDeployment} />);
|
||||
|
||||
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
||||
});
|
||||
@@ -59,7 +46,6 @@ test('should render "updated just now" when the deployment\'s status is DEPLOYED
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
deploymentEndedAt: null,
|
||||
}}
|
||||
appCreatedAt="2023-02-24"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -76,19 +62,8 @@ test('should render "deployed 1 day ago" when the deployment has ended', () => {
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
deploymentEndedAt: '2023-02-24T12:15:00.000Z',
|
||||
}}
|
||||
appCreatedAt="2023-02-24"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/deployed 1 day ago/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render "created 1 day ago" if the application does not have a deployment', () => {
|
||||
vi.setSystemTime(new Date('2023-02-25T12:25:00.000Z'));
|
||||
|
||||
render(
|
||||
<DeploymentStatusMessage deployment={null} appCreatedAt="2023-02-24" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/created 1 day ago/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import { Avatar } from '@/components/ui/v1/Avatar';
|
||||
import { Avatar } from '@/components/ui/v2/Avatar';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { Deployment } from '@/types/application';
|
||||
import formatDistance from 'date-fns/formatDistance';
|
||||
|
||||
export interface DeploymentStatusMessageProps {
|
||||
/**
|
||||
* The deployment to render the status message for.
|
||||
*/
|
||||
deployment: Partial<Deployment>;
|
||||
/**
|
||||
* The date the application was created.
|
||||
*/
|
||||
appCreatedAt: string;
|
||||
}
|
||||
|
||||
export default function DeploymentStatusMessage({
|
||||
deployment,
|
||||
appCreatedAt,
|
||||
}: DeploymentStatusMessageProps) {
|
||||
const isDeployingToProduction = [
|
||||
'SCHEDULED',
|
||||
@@ -29,11 +21,10 @@ export default function DeploymentStatusMessage({
|
||||
(deployment && !deployment.deploymentEndedAt)
|
||||
) {
|
||||
return (
|
||||
<span className="flex flex-row">
|
||||
<span className="flex flex-row justify-start">
|
||||
<Avatar
|
||||
component="span"
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
alt={`Avatar of ${deployment.commitUserName}`}
|
||||
src={deployment.commitUserAvatarUrl}
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text component="span" className="self-center text-sm">
|
||||
@@ -44,30 +35,26 @@ export default function DeploymentStatusMessage({
|
||||
}
|
||||
|
||||
if (!isDeployingToProduction && deployment?.deploymentEndedAt) {
|
||||
const statusMessage = `deployed ${formatDistance(new Date(deployment.deploymentEndedAt), new Date(), { addSuffix: true })}`;
|
||||
|
||||
return (
|
||||
<span className="grid grid-flow-col">
|
||||
<div className="relative flex flex-row">
|
||||
<Avatar
|
||||
component="span"
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
alt={`Avatar of ${deployment.commitUserName}`}
|
||||
src={deployment.commitUserAvatarUrl}
|
||||
className="mr-2 mt-1 h-4 w-4"
|
||||
/>
|
||||
<Text component="span" className="self-center truncate text-sm">
|
||||
{deployment.commitUserName} deployed{' '}
|
||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Text>
|
||||
</span>
|
||||
<div className="flex flex-col text-sm text-muted-foreground">
|
||||
<p className="line-clamp-1 break-all">{deployment.commitUserName}</p>
|
||||
<p>{statusMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text component="span" className="text-sm">
|
||||
created{' '}
|
||||
{formatDistance(new Date(appCreatedAt), new Date(), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
<Text component="span" className="text-sm text-muted-foreground">
|
||||
No deployments
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
GetProjectDocument,
|
||||
useGetProjectQuery,
|
||||
type GetProjectQuery,
|
||||
type ProjectFragment,
|
||||
GetProjectDocument, type GetProjectQuery,
|
||||
type ProjectFragment
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -25,8 +23,7 @@ export interface UseProjectReturnType {
|
||||
}
|
||||
|
||||
export default function useProject({
|
||||
poll = false,
|
||||
target = 'console-next',
|
||||
poll = false
|
||||
}: UseProjectOptions = {}): UseProjectReturnType {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
@@ -44,24 +41,12 @@ export default function useProject({
|
||||
!!appSubdomain &&
|
||||
isRouterReady;
|
||||
|
||||
// Fetch project data for 'console-next' target
|
||||
const {
|
||||
data: consoleData,
|
||||
loading: consoleLoading,
|
||||
error: consoleError,
|
||||
refetch: refetchConsole,
|
||||
} = useGetProjectQuery({
|
||||
variables: { subdomain: appSubdomain as string },
|
||||
skip: !shouldFetchProject && target === 'console-next',
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: poll ? 5000 * 2 : 0, // every 10s
|
||||
});
|
||||
|
||||
// Fetch project data for 'user-project' target using client.graphql
|
||||
const {
|
||||
data: userProjectData,
|
||||
isFetching: userProjectFetching,
|
||||
refetch: refetchUserProject,
|
||||
data,
|
||||
isFetching,
|
||||
refetch,
|
||||
error
|
||||
} = useQuery(
|
||||
['currentProject', appSubdomain],
|
||||
() =>
|
||||
@@ -69,33 +54,19 @@ export default function useProject({
|
||||
subdomain: (appSubdomain as string) || '',
|
||||
}),
|
||||
{
|
||||
enabled: shouldFetchProject,
|
||||
keepPreviousData: true,
|
||||
enabled: shouldFetchProject && target === 'user-project',
|
||||
staleTime: poll ? 5000 : Infinity, // Adjust staleTime for better performance
|
||||
staleTime: poll ? 1000 * 10 : Infinity,
|
||||
},
|
||||
);
|
||||
|
||||
const project =
|
||||
target === 'console-next'
|
||||
? consoleData?.apps?.[0] || null
|
||||
: userProjectData?.data?.apps?.[0] || null;
|
||||
|
||||
const loading =
|
||||
target === 'console-next'
|
||||
? consoleLoading || isAuthLoading
|
||||
: userProjectFetching || isAuthLoading;
|
||||
const error = consoleError
|
||||
? new Error(consoleError.message || 'Unknown error occurred.')
|
||||
: null;
|
||||
|
||||
const refetch =
|
||||
target === 'console-next' ? refetchConsole : refetchUserProject;
|
||||
|
||||
if (isPlatform) {
|
||||
return {
|
||||
project,
|
||||
loading,
|
||||
error,
|
||||
project: data?.data?.apps?.[0] || null,
|
||||
loading: isFetching,
|
||||
error: Array.isArray(error || {})
|
||||
? error[0]
|
||||
: error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
@@ -106,4 +77,4 @@ export default function useProject({
|
||||
error: null,
|
||||
refetch: () => Promise.resolve(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,132 @@
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type { MetricsCardProps } from '@/features/orgs/projects/overview/components/MetricsCard';
|
||||
import { MetricsCard } from '@/features/orgs/projects/overview/components/MetricsCard';
|
||||
import { prettifyNumber } from '@/utils/prettifyNumber';
|
||||
import { prettifySize } from '@/utils/prettifySize';
|
||||
import { useGetProjectMetricsQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
useGetProjectMetricsQuery,
|
||||
useGetProjectRequestsMetricQuery,
|
||||
useGetUserProjectMetricsQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { prettifySize } from '@/utils/prettifySize';
|
||||
import { formatISO, startOfDay, startOfMonth, subMinutes } from 'date-fns';
|
||||
|
||||
const now = new Date();
|
||||
|
||||
export default function OverviewMetrics() {
|
||||
const { project } = useProject();
|
||||
const { data, loading, error } = useGetProjectMetricsQuery({
|
||||
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
|
||||
|
||||
const {
|
||||
data: {
|
||||
allUsers: { aggregate: { count: allUsers = 0 } = {} } = {},
|
||||
dailyActiveUsers: {
|
||||
aggregate: { count: dailyActiveUsers = 0 } = {},
|
||||
} = {},
|
||||
monthlyActiveUsers: {
|
||||
aggregate: { count: monthlyActiveUsers = 0 } = {},
|
||||
} = {},
|
||||
filesAggregate: {
|
||||
aggregate: { sum: { size: totalStorage = 0 } = {} } = {},
|
||||
} = {},
|
||||
} = {},
|
||||
} = useGetUserProjectMetricsQuery({
|
||||
client: remoteProjectGQLClient,
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
startOfMonth: startOfMonth(new Date()),
|
||||
today: startOfDay(new Date()),
|
||||
},
|
||||
skip: !project,
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
totalRequests: { value: totalRequestsInLastFiveMinutes = 0 } = {},
|
||||
} = {},
|
||||
} = useGetProjectRequestsMetricQuery({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
from: formatISO(subMinutes(new Date(), 6)), // 6 mns earlier
|
||||
to: formatISO(subMinutes(new Date(), 1)), // 1 mn earlier
|
||||
},
|
||||
skip: !project,
|
||||
pollInterval: 1000 * 60 * 5, // Poll every 5 minutes
|
||||
});
|
||||
|
||||
const {
|
||||
data: {
|
||||
functionsDuration: { value: functionsDuration = 0 } = {},
|
||||
totalRequests: { value: totalRequests = 0 } = {},
|
||||
postgresVolumeUsage: { value: postgresVolumeUsage = 0 } = {},
|
||||
egressVolume: { value: egressVolume = 0 } = {},
|
||||
} = {},
|
||||
loading,
|
||||
error,
|
||||
} = useGetProjectMetricsQuery({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
subdomain: project?.subdomain,
|
||||
from: new Date(now.getFullYear(), now.getMonth(), 1),
|
||||
},
|
||||
skip: !project?.id,
|
||||
skip: !project,
|
||||
});
|
||||
|
||||
const cardElements: MetricsCardProps[] = [
|
||||
{
|
||||
label: 'CPU Usage Seconds',
|
||||
tooltip: 'Total time the service has used the CPUs',
|
||||
value: prettifyNumber(data?.cpuSecondsUsage?.value || 0),
|
||||
label: 'Daily Active Users',
|
||||
tooltip: 'Unique users active today',
|
||||
value: prettifyNumber(dailyActiveUsers),
|
||||
},
|
||||
{
|
||||
label: 'Monthly Active Users',
|
||||
tooltip: 'Unique users active this month',
|
||||
value: prettifyNumber(monthlyActiveUsers),
|
||||
},
|
||||
{
|
||||
label: 'All Users',
|
||||
tooltip: 'Total registered users',
|
||||
value: prettifyNumber(allUsers),
|
||||
},
|
||||
{
|
||||
label: 'RPS',
|
||||
tooltip: 'Requests Per Second (RPS) measured in the last 5 minutes',
|
||||
value: prettifyNumber(totalRequestsInLastFiveMinutes / 300, {
|
||||
numberOfDecimals: 2,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Total Requests',
|
||||
tooltip:
|
||||
'Total amount of requests your services have received excluding functions',
|
||||
value: prettifyNumber(data?.totalRequests?.value || 0, {
|
||||
numberOfDecimals: data?.totalRequests?.value > 1000 ? 2 : 0,
|
||||
tooltip: 'Total service requests this month so far (excluding functions)',
|
||||
value: prettifyNumber(totalRequests || 0, {
|
||||
numberOfDecimals: totalRequests > 1000 ? 2 : 0,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Function Invocations',
|
||||
tooltip: 'Number of times your functions have been called',
|
||||
value: prettifyNumber(data?.functionInvocations?.value || 0, {
|
||||
numberOfDecimals: 0,
|
||||
}),
|
||||
label: 'Egress',
|
||||
tooltip: 'Total outgoing data transfer this month so far',
|
||||
value: prettifySize(egressVolume),
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
tooltip: 'Amount of logs stored',
|
||||
value: prettifySize(data?.logsVolume?.value || 0),
|
||||
label: 'Functions Duration',
|
||||
tooltip: 'Total Functions execution this month so far',
|
||||
value: prettifyNumber(functionsDuration),
|
||||
},
|
||||
{
|
||||
label: 'Storage',
|
||||
tooltip: 'Total size of stored files in the storage service',
|
||||
value: prettifySize(totalStorage || 0),
|
||||
},
|
||||
{
|
||||
label: 'Postgres Volume Usage',
|
||||
tooltip: 'Used storage in the Postgres database',
|
||||
value: prettifySize(postgresVolumeUsage),
|
||||
},
|
||||
];
|
||||
|
||||
if (!data && error) {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ export default function useNotFoundRedirect() {
|
||||
router.pathname === '/account' ||
|
||||
router.pathname === '/support/ticket' ||
|
||||
router.pathname === '/run-one-click-install' ||
|
||||
router.pathname.includes('/orgs/_') ||
|
||||
router.pathname.includes('/orgs/_/projects/_') ||
|
||||
orgSlug ||
|
||||
(orgSlug && appSubdomain) ||
|
||||
// If we are on a valid workspace and project, we don't want to redirect to 404
|
||||
|
||||
9
dashboard/src/gql/app/getProjectRequestsMetric.gql
Normal file
9
dashboard/src/gql/app/getProjectRequestsMetric.gql
Normal file
@@ -0,0 +1,9 @@
|
||||
query GetProjectRequestsMetric(
|
||||
$appId: String!
|
||||
$from: Timestamp
|
||||
$to: Timestamp
|
||||
) {
|
||||
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
|
||||
value
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
query getAssistants {
|
||||
query getAssistants($isFileStoresSupported: Boolean!) {
|
||||
graphite {
|
||||
assistants {
|
||||
assistantID
|
||||
@@ -28,6 +28,7 @@ query getAssistants {
|
||||
required
|
||||
}
|
||||
}
|
||||
fileStores @include(if: $isFileStoresSupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation deleteFileStore($id: uuid!) {
|
||||
graphite {
|
||||
deleteFileStore(id: $id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
query getGraphiteFileStores {
|
||||
graphite {
|
||||
fileStores {
|
||||
id
|
||||
name
|
||||
vectorStoreID
|
||||
buckets
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation insertFileStore($object: graphiteFileStoreInput!) {
|
||||
graphite {
|
||||
insertFileStore(object: $object) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation updateFileStore($id: uuid!, $object: graphiteFileStoreInput!) {
|
||||
graphite {
|
||||
updateFileStore(id: $id, object: $object) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ query getProjects($orgSlug: String!) {
|
||||
slug
|
||||
createdAt
|
||||
subdomain
|
||||
region {
|
||||
id
|
||||
name
|
||||
}
|
||||
deployments(limit: 4, order_by: { deploymentStartedAt: desc }) {
|
||||
id
|
||||
commitSHA
|
||||
@@ -20,5 +24,12 @@ query getProjects($orgSlug: String!) {
|
||||
email
|
||||
displayName
|
||||
}
|
||||
appStates(order_by: { createdAt: desc }, limit: 1) {
|
||||
id
|
||||
appId
|
||||
message
|
||||
stateId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
dashboard/src/gql/organizations/getUserProjectMetrics.gql
Normal file
28
dashboard/src/gql/organizations/getUserProjectMetrics.gql
Normal file
@@ -0,0 +1,28 @@
|
||||
query GetUserProjectMetrics($startOfMonth: timestamptz!, $today: timestamptz!) {
|
||||
monthlyActiveUsers: usersAggregate(
|
||||
where: { lastSeen: { _gte: $startOfMonth, _lte: $today } }
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
dailyActiveUsers: usersAggregate(where: { lastSeen: { _gte: $today } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
allUsers: usersAggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
filesAggregate {
|
||||
aggregate {
|
||||
count
|
||||
sum {
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { useIsGraphiteEnabled } from '@/features/projects/common/hooks/useIsGrap
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
useGetAssistantsQuery,
|
||||
type GetAssistantsQuery,
|
||||
type GetAssistantsQuery
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
|
||||
@@ -29,21 +29,29 @@ export type Assistant = Omit<
|
||||
export default function AssistantsPage() {
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
||||
|
||||
const { data, loading, refetch } = useGetAssistantsQuery({
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
refetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
|
||||
|
||||
const assistants = useMemo(
|
||||
() => data?.graphite?.assistants || [],
|
||||
[data],
|
||||
);
|
||||
|
||||
const openCreateAssistantForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new Assistant',
|
||||
component: <AssistantForm onSubmit={refetch} />,
|
||||
component: (
|
||||
<AssistantForm onSubmit={refetch} />
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,7 +105,11 @@ export default function AssistantsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphite?.assistants.length === 0 && !loading) {
|
||||
if (loading) {
|
||||
return <Box className="p-4">Loading...</Box>;
|
||||
}
|
||||
|
||||
if (assistants.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
@@ -141,13 +153,11 @@ export default function AssistantsPage() {
|
||||
New
|
||||
</Button>
|
||||
</Box>
|
||||
<div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
|
||||
import {
|
||||
useGetAssistantsQuery,
|
||||
useGetGraphiteFileStoresQuery,
|
||||
type GetAssistantsQuery,
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
@@ -21,6 +22,7 @@ import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import { AssistantsList } from '@/features/orgs/projects/ai/AssistantsList';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
@@ -43,24 +45,46 @@ export default function AssistantsPage() {
|
||||
const { isGraphiteEnabled, loading: loadingGraphite } =
|
||||
useIsGraphiteEnabled();
|
||||
|
||||
const {
|
||||
data,
|
||||
loading: loadingAssistants,
|
||||
refetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
const { isFileStoreSupported, loading: fileStoreLoading } =
|
||||
useIsFileStoreSupported();
|
||||
|
||||
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
|
||||
const {
|
||||
data: assistantsData,
|
||||
loading: assistantsLoading,
|
||||
refetch: assistantsRefetch,
|
||||
} = useGetAssistantsQuery({
|
||||
client: adminClient,
|
||||
variables: {
|
||||
isFileStoresSupported: isFileStoreSupported ?? false,
|
||||
},
|
||||
skip: isFileStoreSupported === null || fileStoreLoading,
|
||||
});
|
||||
const { data: fileStoresData } = useGetGraphiteFileStoresQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const assistants = useMemo(
|
||||
() => assistantsData?.graphite?.assistants || [],
|
||||
[assistantsData],
|
||||
);
|
||||
const fileStores = useMemo(
|
||||
() => fileStoresData?.graphite?.fileStores || [],
|
||||
[fileStoresData],
|
||||
);
|
||||
|
||||
const openCreateAssistantForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new Assistant',
|
||||
component: <AssistantForm onSubmit={refetch} />,
|
||||
component: (
|
||||
<AssistantForm
|
||||
onSubmit={assistantsRefetch}
|
||||
fileStores={isFileStoreSupported ? fileStores : undefined}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
if (loadingOrg || loadingProject || loadingGraphite || loadingAssistants) {
|
||||
if (loadingOrg || loadingProject || loadingGraphite || assistantsLoading) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center w-full h-full">
|
||||
<ActivityIndicator
|
||||
@@ -114,7 +138,7 @@ export default function AssistantsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphite?.assistants.length === 0 && !loadingAssistants) {
|
||||
if (assistants.length === 0 && !assistantsLoading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
@@ -161,8 +185,9 @@ export default function AssistantsPage() {
|
||||
<div>
|
||||
<AssistantsList
|
||||
assistants={assistants}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
fileStores={isFileStoreSupported ? fileStores : undefined}
|
||||
onDelete={() => assistantsRefetch()}
|
||||
onCreateOrUpdate={() => assistantsRefetch()}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
@@ -12,14 +12,13 @@ import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
// import AILayout from '@/features/orgs/layout/AILayout/AILayout';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { AutoEmbeddingsForm } from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
||||
import { AutoEmbeddingsList } from '@/features/orgs/projects/ai/AutoEmbeddingsList';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
useGetGraphiteAutoEmbeddingsConfigurationsQuery,
|
||||
@@ -36,9 +35,11 @@ export type AutoEmbeddingsConfiguration = Omit<
|
||||
export default function AutoEmbeddingsPage() {
|
||||
const limit = useRef(25);
|
||||
const router = useRouter();
|
||||
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { currentOrg: org } = useOrgs();
|
||||
|
||||
const { org } = useCurrentOrg();
|
||||
const { project } = useProject();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
@@ -128,7 +129,7 @@ export default function AutoEmbeddingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.graphiteAutoEmbeddingsConfigurations.length === 0 && !loading) {
|
||||
if (autoEmbeddingsConfigurations.length === 0 && !loading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full p-6"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { FileStoresIcon } from '@/components/ui/v2/icons/FileStoresIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { FileStoreForm } from '@/features/orgs/projects/ai/FileStoreForm';
|
||||
import { FileStoresList } from '@/features/orgs/projects/ai/FileStoresList';
|
||||
import { useIsFileStoreSupported } from '@/features/orgs/projects/common/hooks/useIsFileStoreSupported';
|
||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import {
|
||||
useGetGraphiteFileStoresQuery,
|
||||
type GetGraphiteFileStoresQuery
|
||||
} from '@/utils/__generated__/graphite.graphql';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
|
||||
export type GraphiteFileStore = Omit<
|
||||
GetGraphiteFileStoresQuery['graphite']['fileStores'][0],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
export default function FileStoresPage() {
|
||||
const { openDrawer } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { org, loading: loadingOrg } = useCurrentOrg();
|
||||
const { project, loading: loadingProject } = useProject();
|
||||
|
||||
const { adminClient } = useAdminApolloClient();
|
||||
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
||||
const { isFileStoreSupported } = useIsFileStoreSupported();
|
||||
|
||||
const { data, loading, refetch } = useGetGraphiteFileStoresQuery({
|
||||
client: adminClient,
|
||||
});
|
||||
|
||||
const fileStores = useMemo(() => data?.graphite.fileStores || [], [data]);
|
||||
|
||||
const openCreateFileStoreForm = () => {
|
||||
openDrawer({
|
||||
title: 'Create a new File Store',
|
||||
component: <FileStoreForm onSubmit={refetch} />,
|
||||
});
|
||||
};
|
||||
|
||||
if (loadingOrg || loadingProject || loading) {
|
||||
return (
|
||||
<Box className="flex items-center justify-center w-full h-full">
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading File Stores..."
|
||||
className="justify-center"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (isPlatform && org?.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 (
|
||||
(isPlatform &&
|
||||
!org?.plan?.isFree &&
|
||||
!project.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full 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={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
AI Settings
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Text>
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (fileStores.length === 0 && !loading) {
|
||||
return (
|
||||
<Box
|
||||
className="w-full 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">
|
||||
<FileStoresIcon className="h-10 w-10" />
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Text className="text-center font-medium" variant="h3">
|
||||
No File Stores are configured
|
||||
</Text>
|
||||
<Text variant="subtitle1" className="text-center">
|
||||
File Stores are used to share storage documents with your
|
||||
AI assistants.
|
||||
</Text>
|
||||
{!isFileStoreSupported && (
|
||||
<Box className="px-4 pb-4">
|
||||
<Alert className="mt-2 text-left">
|
||||
Please upgrade Graphite to its latest version in order to use
|
||||
file stores.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between rounded-lg">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className="w-full"
|
||||
onClick={openCreateFileStoreForm}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
disabled={!isFileStoreSupported}
|
||||
>
|
||||
Add a new File Store
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col w-full overflow-hidden">
|
||||
<Box className="flex flex-row place-content-end border-b-1 p-4">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openCreateFileStoreForm}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
</Box>
|
||||
<div>
|
||||
<FileStoresList
|
||||
fileStores={fileStores}
|
||||
onDelete={() => refetch()}
|
||||
onCreateOrUpdate={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
FileStoresPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<ProjectLayout
|
||||
mainContainerProps={{ className: 'flex flex-row w-full h-full' }}
|
||||
>
|
||||
<AISidebar className="w-full max-w-sidebar" />
|
||||
<RetryableErrorBoundary>{page}</RetryableErrorBoundary>
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
};
|
||||
15
dashboard/src/pages/orgs/_/[...slug].tsx
Normal file
15
dashboard/src/pages/orgs/_/[...slug].tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { SelectOrg } from '@/components/common/SelectOrg';
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export default function SelectOrganization() {
|
||||
return <SelectOrg />
|
||||
}
|
||||
|
||||
SelectOrganization.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Select an Organization">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
|
||||
15
dashboard/src/pages/orgs/_/projects/_/[...slug].tsx
Normal file
15
dashboard/src/pages/orgs/_/projects/_/[...slug].tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { SelectOrgAndProject } from '@/components/common/SelectOrgAndProject';
|
||||
|
||||
export default function OrganizationAndProject() {
|
||||
return <SelectOrgAndProject />
|
||||
}
|
||||
|
||||
OrganizationAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Select a Project">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
14
dashboard/src/pages/orgs/_/projects/_/index.tsx
Normal file
14
dashboard/src/pages/orgs/_/projects/_/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { SelectOrgAndProject } from '@/components/common/SelectOrgAndProject';
|
||||
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
|
||||
import { } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export default function SelectOrganizationAndProject() {
|
||||
return <SelectOrgAndProject />
|
||||
}
|
||||
|
||||
SelectOrganizationAndProject.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<AuthenticatedLayout title="Select a Project">{page}</AuthenticatedLayout>
|
||||
);
|
||||
};
|
||||
@@ -290,10 +290,10 @@ function TicketPage() {
|
||||
<ControlledAutocomplete
|
||||
id="services"
|
||||
name="services"
|
||||
label="services"
|
||||
label="Services"
|
||||
fullWidth
|
||||
multiple
|
||||
aria-label="Enabled APIs"
|
||||
aria-label="Services"
|
||||
options={[
|
||||
'Dashboard',
|
||||
'Database',
|
||||
|
||||
22227
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
22227
dashboard/src/utils/__generated__/graphite.graphql.ts
generated
File diff suppressed because it is too large
Load Diff
156
dashboard/src/utils/__generated__/graphql.ts
generated
156
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -2946,6 +2946,7 @@ export type ConfigSystemConfig = {
|
||||
__typename?: 'ConfigSystemConfig';
|
||||
auth?: Maybe<ConfigSystemConfigAuth>;
|
||||
graphql?: Maybe<ConfigSystemConfigGraphql>;
|
||||
persistentVolumesEncrypted?: Maybe<Scalars['Boolean']>;
|
||||
postgres: ConfigSystemConfigPostgres;
|
||||
};
|
||||
|
||||
@@ -3015,6 +3016,7 @@ export type ConfigSystemConfigComparisonExp = {
|
||||
_or?: InputMaybe<Array<ConfigSystemConfigComparisonExp>>;
|
||||
auth?: InputMaybe<ConfigSystemConfigAuthComparisonExp>;
|
||||
graphql?: InputMaybe<ConfigSystemConfigGraphqlComparisonExp>;
|
||||
persistentVolumesEncrypted?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
postgres?: InputMaybe<ConfigSystemConfigPostgresComparisonExp>;
|
||||
};
|
||||
|
||||
@@ -3045,6 +3047,7 @@ export type ConfigSystemConfigGraphqlUpdateInput = {
|
||||
export type ConfigSystemConfigInsertInput = {
|
||||
auth?: InputMaybe<ConfigSystemConfigAuthInsertInput>;
|
||||
graphql?: InputMaybe<ConfigSystemConfigGraphqlInsertInput>;
|
||||
persistentVolumesEncrypted?: InputMaybe<Scalars['Boolean']>;
|
||||
postgres: ConfigSystemConfigPostgresInsertInput;
|
||||
};
|
||||
|
||||
@@ -3143,6 +3146,7 @@ export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
export type ConfigSystemConfigUpdateInput = {
|
||||
auth?: InputMaybe<ConfigSystemConfigAuthUpdateInput>;
|
||||
graphql?: InputMaybe<ConfigSystemConfigGraphqlUpdateInput>;
|
||||
persistentVolumesEncrypted?: InputMaybe<Scalars['Boolean']>;
|
||||
postgres?: InputMaybe<ConfigSystemConfigPostgresUpdateInput>;
|
||||
};
|
||||
|
||||
@@ -4337,6 +4341,7 @@ export type Apps = {
|
||||
appStates: Array<AppStateHistory>;
|
||||
/** An aggregate relationship */
|
||||
appStates_aggregate: AppStateHistory_Aggregate;
|
||||
automaticDeploys: Scalars['Boolean'];
|
||||
/** An array relationship */
|
||||
backups: Array<Backups>;
|
||||
/** An aggregate relationship */
|
||||
@@ -4615,6 +4620,7 @@ export type Apps_Bool_Exp = {
|
||||
_or?: InputMaybe<Array<Apps_Bool_Exp>>;
|
||||
appStates?: InputMaybe<AppStateHistory_Bool_Exp>;
|
||||
appStates_aggregate?: InputMaybe<AppStateHistory_Aggregate_Bool_Exp>;
|
||||
automaticDeploys?: InputMaybe<Boolean_Comparison_Exp>;
|
||||
backups?: InputMaybe<Backups_Bool_Exp>;
|
||||
backups_aggregate?: InputMaybe<Backups_Aggregate_Bool_Exp>;
|
||||
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Bool_Exp>;
|
||||
@@ -4692,6 +4698,7 @@ export type Apps_Inc_Input = {
|
||||
/** input type for inserting data into table "apps" */
|
||||
export type Apps_Insert_Input = {
|
||||
appStates?: InputMaybe<AppStateHistory_Arr_Rel_Insert_Input>;
|
||||
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
|
||||
backups?: InputMaybe<Backups_Arr_Rel_Insert_Input>;
|
||||
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Obj_Rel_Insert_Input>;
|
||||
billingSubscriptions?: InputMaybe<Billing_Subscriptions_Obj_Rel_Insert_Input>;
|
||||
@@ -4858,6 +4865,7 @@ export type Apps_On_Conflict = {
|
||||
/** Ordering options when selecting data from "apps". */
|
||||
export type Apps_Order_By = {
|
||||
appStates_aggregate?: InputMaybe<AppStateHistory_Aggregate_Order_By>;
|
||||
automaticDeploys?: InputMaybe<Order_By>;
|
||||
backups_aggregate?: InputMaybe<Backups_Aggregate_Order_By>;
|
||||
billingDedicatedCompute?: InputMaybe<Billing_Dedicated_Compute_Order_By>;
|
||||
billingSubscriptions?: InputMaybe<Billing_Subscriptions_Order_By>;
|
||||
@@ -4909,6 +4917,8 @@ export type Apps_Prepend_Input = {
|
||||
|
||||
/** select columns of table "apps" */
|
||||
export enum Apps_Select_Column {
|
||||
/** column name */
|
||||
AutomaticDeploys = 'automaticDeploys',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
@@ -4961,6 +4971,8 @@ export enum Apps_Select_Column {
|
||||
|
||||
/** select "apps_aggregate_bool_exp_bool_and_arguments_columns" columns of table "apps" */
|
||||
export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_And_Arguments_Columns {
|
||||
/** column name */
|
||||
AutomaticDeploys = 'automaticDeploys',
|
||||
/** column name */
|
||||
IsLocked = 'isLocked',
|
||||
/** column name */
|
||||
@@ -4969,6 +4981,8 @@ export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_And_Arguments_Column
|
||||
|
||||
/** select "apps_aggregate_bool_exp_bool_or_arguments_columns" columns of table "apps" */
|
||||
export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_Or_Arguments_Columns {
|
||||
/** column name */
|
||||
AutomaticDeploys = 'automaticDeploys',
|
||||
/** column name */
|
||||
IsLocked = 'isLocked',
|
||||
/** column name */
|
||||
@@ -4977,6 +4991,7 @@ export enum Apps_Select_Column_Apps_Aggregate_Bool_Exp_Bool_Or_Arguments_Columns
|
||||
|
||||
/** input type for updating data in table "apps" */
|
||||
export type Apps_Set_Input = {
|
||||
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
creatorUserId?: InputMaybe<Scalars['uuid']>;
|
||||
currentState?: InputMaybe<Scalars['Int']>;
|
||||
@@ -5051,6 +5066,7 @@ export type Apps_Stream_Cursor_Input = {
|
||||
|
||||
/** Initial value of the column from where the streaming should start */
|
||||
export type Apps_Stream_Cursor_Value_Input = {
|
||||
automaticDeploys?: InputMaybe<Scalars['Boolean']>;
|
||||
createdAt?: InputMaybe<Scalars['timestamptz']>;
|
||||
creatorUserId?: InputMaybe<Scalars['uuid']>;
|
||||
currentState?: InputMaybe<Scalars['Int']>;
|
||||
@@ -5092,6 +5108,8 @@ export type Apps_Sum_Order_By = {
|
||||
|
||||
/** update columns of table "apps" */
|
||||
export enum Apps_Update_Column {
|
||||
/** column name */
|
||||
AutomaticDeploys = 'automaticDeploys',
|
||||
/** column name */
|
||||
CreatedAt = 'createdAt',
|
||||
/** column name */
|
||||
@@ -13334,6 +13352,7 @@ export type Mutation_Root = {
|
||||
delete_regions?: Maybe<Regions_Mutation_Response>;
|
||||
/** delete single row from the table: "regions" */
|
||||
delete_regions_by_pk?: Maybe<Regions>;
|
||||
encryptPersistentVolumes: Scalars['Boolean'];
|
||||
/** insert a single row into the table: "announcements_read" */
|
||||
insertAnnouncementRead?: Maybe<Announcements_Read>;
|
||||
/** insert data into the table: "announcements_read" */
|
||||
@@ -14659,6 +14678,12 @@ export type Mutation_RootDelete_Regions_By_PkArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootEncryptPersistentVolumesArgs = {
|
||||
appID: Scalars['uuid'];
|
||||
};
|
||||
|
||||
|
||||
/** mutation root */
|
||||
export type Mutation_RootInsertAnnouncementReadArgs = {
|
||||
object: Announcements_Read_Insert_Input;
|
||||
@@ -27400,6 +27425,15 @@ export type GetProjectMetricsQueryVariables = Exact<{
|
||||
|
||||
export type GetProjectMetricsQuery = { __typename?: 'query_root', logsVolume: { __typename?: 'Metrics', value: any }, cpuSecondsUsage: { __typename?: 'Metrics', value: any }, functionInvocations: { __typename?: 'Metrics', value: any }, functionsDuration: { __typename?: 'Metrics', value: any }, postgresVolumeCapacity: { __typename?: 'Metrics', value: any }, postgresVolumeUsage: { __typename?: 'Metrics', value: any }, totalRequests: { __typename?: 'Metrics', value: any }, egressVolume: { __typename?: 'Metrics', value: any } };
|
||||
|
||||
export type GetProjectRequestsMetricQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
from?: InputMaybe<Scalars['Timestamp']>;
|
||||
to?: InputMaybe<Scalars['Timestamp']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectRequestsMetricQuery = { __typename?: 'query_root', totalRequests: { __typename?: 'Metrics', value: any } };
|
||||
|
||||
export type GetProjectServicesHealthQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
}>;
|
||||
@@ -27840,7 +27874,15 @@ export type GetProjectsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectsQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, name: string, slug: string, createdAt: any, subdomain: string, 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 GetProjectsQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, name: string, slug: string, createdAt: any, subdomain: string, region: { __typename?: 'regions', id: any, name: string }, 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, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }> }> };
|
||||
|
||||
export type GetUserProjectMetricsQueryVariables = Exact<{
|
||||
startOfMonth: Scalars['timestamptz'];
|
||||
today: Scalars['timestamptz'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetUserProjectMetricsQuery = { __typename?: 'query_root', monthlyActiveUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, dailyActiveUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, allUsers: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, filesAggregate: { __typename?: 'files_aggregate', aggregate?: { __typename?: 'files_aggregate_fields', count: number, sum?: { __typename?: 'files_sum_fields', size?: number | null } | null } | null } };
|
||||
|
||||
export type InsertOrgApplicationMutationVariables = Exact<{
|
||||
app: Apps_Insert_Input;
|
||||
@@ -29925,6 +29967,46 @@ export type GetProjectMetricsQueryResult = Apollo.QueryResult<GetProjectMetricsQ
|
||||
export function refetchGetProjectMetricsQuery(variables: GetProjectMetricsQueryVariables) {
|
||||
return { query: GetProjectMetricsDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectRequestsMetricDocument = gql`
|
||||
query GetProjectRequestsMetric($appId: String!, $from: Timestamp, $to: Timestamp) {
|
||||
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetProjectRequestsMetricQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetProjectRequestsMetricQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetProjectRequestsMetricQuery` 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 } = useGetProjectRequestsMetricQuery({
|
||||
* variables: {
|
||||
* appId: // value for 'appId'
|
||||
* from: // value for 'from'
|
||||
* to: // value for 'to'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetProjectRequestsMetricQuery(baseOptions: Apollo.QueryHookOptions<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>(GetProjectRequestsMetricDocument, options);
|
||||
}
|
||||
export function useGetProjectRequestsMetricLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>(GetProjectRequestsMetricDocument, options);
|
||||
}
|
||||
export type GetProjectRequestsMetricQueryHookResult = ReturnType<typeof useGetProjectRequestsMetricQuery>;
|
||||
export type GetProjectRequestsMetricLazyQueryHookResult = ReturnType<typeof useGetProjectRequestsMetricLazyQuery>;
|
||||
export type GetProjectRequestsMetricQueryResult = Apollo.QueryResult<GetProjectRequestsMetricQuery, GetProjectRequestsMetricQueryVariables>;
|
||||
export function refetchGetProjectRequestsMetricQuery(variables: GetProjectRequestsMetricQueryVariables) {
|
||||
return { query: GetProjectRequestsMetricDocument, variables: variables }
|
||||
}
|
||||
export const GetProjectServicesHealthDocument = gql`
|
||||
query getProjectServicesHealth($appId: String!) {
|
||||
getProjectStatus(appID: $appId) {
|
||||
@@ -32463,6 +32545,10 @@ export const GetProjectsDocument = gql`
|
||||
slug
|
||||
createdAt
|
||||
subdomain
|
||||
region {
|
||||
id
|
||||
name
|
||||
}
|
||||
deployments(limit: 4, order_by: {deploymentStartedAt: desc}) {
|
||||
id
|
||||
commitSHA
|
||||
@@ -32478,6 +32564,13 @@ export const GetProjectsDocument = gql`
|
||||
email
|
||||
displayName
|
||||
}
|
||||
appStates(order_by: {createdAt: desc}, limit: 1) {
|
||||
id
|
||||
appId
|
||||
message
|
||||
stateId
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -32512,6 +32605,67 @@ export type GetProjectsQueryResult = Apollo.QueryResult<GetProjectsQuery, GetPro
|
||||
export function refetchGetProjectsQuery(variables: GetProjectsQueryVariables) {
|
||||
return { query: GetProjectsDocument, variables: variables }
|
||||
}
|
||||
export const GetUserProjectMetricsDocument = gql`
|
||||
query GetUserProjectMetrics($startOfMonth: timestamptz!, $today: timestamptz!) {
|
||||
monthlyActiveUsers: usersAggregate(
|
||||
where: {lastSeen: {_gte: $startOfMonth, _lte: $today}}
|
||||
) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
dailyActiveUsers: usersAggregate(where: {lastSeen: {_gte: $today}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
allUsers: usersAggregate {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
filesAggregate {
|
||||
aggregate {
|
||||
count
|
||||
sum {
|
||||
size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetUserProjectMetricsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetUserProjectMetricsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetUserProjectMetricsQuery` 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 } = useGetUserProjectMetricsQuery({
|
||||
* variables: {
|
||||
* startOfMonth: // value for 'startOfMonth'
|
||||
* today: // value for 'today'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetUserProjectMetricsQuery(baseOptions: Apollo.QueryHookOptions<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>(GetUserProjectMetricsDocument, options);
|
||||
}
|
||||
export function useGetUserProjectMetricsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>(GetUserProjectMetricsDocument, options);
|
||||
}
|
||||
export type GetUserProjectMetricsQueryHookResult = ReturnType<typeof useGetUserProjectMetricsQuery>;
|
||||
export type GetUserProjectMetricsLazyQueryHookResult = ReturnType<typeof useGetUserProjectMetricsLazyQuery>;
|
||||
export type GetUserProjectMetricsQueryResult = Apollo.QueryResult<GetUserProjectMetricsQuery, GetUserProjectMetricsQueryVariables>;
|
||||
export function refetchGetUserProjectMetricsQuery(variables: GetUserProjectMetricsQueryVariables) {
|
||||
return { query: GetUserProjectMetricsDocument, variables: variables }
|
||||
}
|
||||
export const InsertOrgApplicationDocument = gql`
|
||||
mutation insertOrgApplication($app: apps_insert_input!) {
|
||||
insertApp(object: $app) {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a99f034: chore: fix function name
|
||||
|
||||
## 2.23.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 14e6100: feat: add documentation for sign-in with ID token
|
||||
|
||||
## 2.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -34,6 +34,9 @@ nhost
|
||||
│ │ ├── password-reset
|
||||
│ │ │ ├── body.html
|
||||
│ │ │ └── subject.txt
|
||||
│ │ ├── signin-otp
|
||||
│ │ │ ├── body.html
|
||||
│ │ │ └── subject.txt
|
||||
│ │ ├── signin-passwordless
|
||||
│ │ │ ├── body.html
|
||||
│ │ │ └── subject.txt
|
||||
@@ -49,6 +52,9 @@ nhost
|
||||
│ ├── password-reset
|
||||
│ │ ├── body.html
|
||||
│ │ └── subject.txt
|
||||
│ ├── signin-otp
|
||||
│ │ ├── body.html
|
||||
│ │ └── subject.txt
|
||||
│ ├── signin-passwordless
|
||||
│ │ ├── body.html
|
||||
│ │ └── subject.txt
|
||||
@@ -82,7 +88,7 @@ The following variables are available to all email templates:
|
||||
| `serverUrl` | URL of the authentication server |
|
||||
| `clientUrl` | URL of your client app |
|
||||
| `redirectTo` | URL where the user will be redirected to after clicking the link and finishing the action of the email |
|
||||
| `ticket` | Ticket that is used to authorize the link request |
|
||||
| `ticket` | Ticket or OTP that is used to authorize the request. |
|
||||
| `displayName` | The display name of the user |
|
||||
| `email` | The email of the user |
|
||||
| `locale` | Locale of the user as a two-letter language code (e.g. "en") |
|
||||
|
||||
93
docs/guides/auth/sign-in-idtokens.mdx
Normal file
93
docs/guides/auth/sign-in-idtokens.mdx
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Sign In with ID tokens
|
||||
sidebarTitle: IDTokens
|
||||
description: Learn about ID tokens
|
||||
icon: binary
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
ID tokens are tokens provided by identity providers that contain authenticated user information and are specifically designed for authentication purposes, unlike access tokens which are used for authorization. ID tokens include claims about the user's identity, such as user ID, name, and email, along with metadata like token expiration time and intended audience.
|
||||
|
||||
ID tokens serve as a secure proof that a user has already been authenticated by a trusted identity provider. When someone logs in through their device's built-in authentication (like Sign in with Apple on iOS/macOS or Google Sign-in on Android), the system generates an ID token. This token can then be passed to your authentication service, confirming the user's identity without requiring them to log in again. This streamlined approach works with any OpenID Connect (OIDC) provider, including popular services like Google One Tap sign-in, making the authentication process both secure and user-friendly.
|
||||
|
||||
## Usage
|
||||
|
||||
To use ID tokens, you need to configure supported identity providers (currently [apple](/guides/auth/social/sign-in-apple) and [google](/guides/auth/social/sign-in-google)) and make sure the `audience` is set correctly.
|
||||
|
||||
### Sign in
|
||||
|
||||
Once everything is configured you can use an ID token to authenticate users with just a single call:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.signInIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="react">See [react docs](/reference/react/use-sign-in-id-token) for details</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="vue">See [vue docs](/reference/react/use-sign-in-id-token) for details</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.signInIdToken(provider: 'google', idToken: '...', nonce: '...');
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Link Provider to existing user
|
||||
|
||||
Similarly to the [Social Connect](/guides/auth/social-connect) feature, you can link an identity provider to an existing user:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="javascript">
|
||||
```js
|
||||
nhost.auth.linkIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
{' '}
|
||||
<Note>Keep in mind this is an authenticated method so the user must be logged in already.</Note>
|
||||
|
||||
</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="react">See [react docs](/reference/react/use-link-id-token) for details</Tab>
|
||||
|
||||
{' '}
|
||||
<Tab title="vue">See [vue docs](/reference/vue/use-link-id-token) for details</Tab>
|
||||
|
||||
<Tab title="dart">
|
||||
```dart
|
||||
nhost.auth.linkIdToken(provider: 'google', idToken: '...', nonce: '...');
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Examples
|
||||
|
||||
Below you can find some examples on how to extract an ID Token from various identity providers to be used with the Auth service. Keep in mind these are just some examples, use cases and sources are not limited to the examples below.
|
||||
|
||||
### React Native
|
||||
|
||||
#### Apple
|
||||
|
||||
For an example on how to authenticate using "Sign in with Apple" on iOS using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithAppleButton.tsx).
|
||||
|
||||
#### Google
|
||||
|
||||
For an example on how to authenticate using "Sign in with Google" on Android using React Native you can refer to our [sample component](https://github.com/nhost/nhost/blob/main/examples/react_native/src/components/SignInWithGoogleButton.tsx).
|
||||
@@ -40,7 +40,7 @@ The domains in the URLs above will all return the IP address for localhost, `127
|
||||
local.auth.local.nhost.run has address 127.0.0.1
|
||||
```
|
||||
|
||||
However, those URLs are powered by a dynamic DNS that can return any IPv4 address you need, you just need to replace the subdomain `local` with a `subdomain` that contains the 4 octets of the IPv4 adress you want separated by `-`. For instance:
|
||||
However, those URLs are powered by a dynamic DNS that can return any IPv4 address you need, you just need to replace the subdomain `local` with a `subdomain` that contains the 4 octets of the IPv4 address you want separated by `-`. For instance:
|
||||
|
||||
```
|
||||
> host 192-168-100-1.auth.local.nhost.run
|
||||
@@ -52,6 +52,13 @@ However, those URLs are powered by a dynamic DNS that can return any IPv4 addres
|
||||
|
||||
This is useful if you need to connect to your environment from a different device, a VM or a mobile device emulator.
|
||||
|
||||
<Warning>
|
||||
Some ISPs filter DNS responses that point to localhost and/or private IP space. If your provider is one of them you may have troubles accessing your local dev environment. As a workaround you have two options:
|
||||
|
||||
1. Follow the instructions under [offline access](/guides/cli/subdomain#offline-access) to create static DNS entries in your machine.
|
||||
2. Configure your computer to use a different [DNS provider](https://privacysavvy.com/security/business/best-free-public-dns-servers/).
|
||||
</Warning>
|
||||
|
||||
To make use of this functionality you can start your development environment after setting the environment variable `NHOST_LOCAL_SUBDOMAIN` or passing the flag `--local-subdomain` :
|
||||
|
||||
```
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Documentation",
|
||||
"openapi": ["reference/openapi-auth.yaml", "reference/openapi-auth-old.yaml", "reference/openapi-storage.yaml"],
|
||||
"openapi": [
|
||||
"reference/openapi-auth.yaml",
|
||||
"reference/openapi-auth-old.yaml",
|
||||
"reference/openapi-storage.yaml"
|
||||
],
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg"
|
||||
@@ -158,6 +162,7 @@
|
||||
"guides/auth/sign-in-magic-link",
|
||||
"guides/auth/sign-in-phone-number",
|
||||
"guides/auth/sign-in-webauthn",
|
||||
"guides/auth/sign-in-idtokens",
|
||||
"guides/auth/elevated-permissions",
|
||||
"guides/auth/bot-protection",
|
||||
"guides/auth/email-templates",
|
||||
@@ -347,7 +352,9 @@
|
||||
"reference/javascript/auth/elevate-email-security-key",
|
||||
"reference/javascript/auth/connect-provider",
|
||||
"reference/javascript/auth/sign-in-email-otp",
|
||||
"reference/javascript/auth/verify-email-otp"
|
||||
"reference/javascript/auth/verify-email-otp",
|
||||
"reference/javascript/auth/sign-in-id-token",
|
||||
"reference/javascript/auth/link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -431,7 +438,9 @@
|
||||
"reference/react/use-user-is-anonymous",
|
||||
"reference/react/use-user-locale",
|
||||
"reference/react/use-user-roles",
|
||||
"reference/react/use-sign-in-email-otp"
|
||||
"reference/react/use-sign-in-email-otp",
|
||||
"reference/react/use-sign-in-id-token",
|
||||
"reference/react/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -479,7 +488,9 @@
|
||||
"reference/nextjs/use-user-is-anonymous",
|
||||
"reference/nextjs/use-user-locale",
|
||||
"reference/nextjs/use-user-roles",
|
||||
"reference/nextjs/use-sign-in-email-otp"
|
||||
"reference/nextjs/use-sign-in-email-otp",
|
||||
"reference/nextjs/use-sign-in-id-token",
|
||||
"reference/nextjs/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -522,7 +533,9 @@
|
||||
"reference/vue/use-elevate-security-key-email",
|
||||
"reference/vue/use-sign-in-email-security-key",
|
||||
"reference/vue/use-sign-up-email-security-key",
|
||||
"reference/vue/use-sign-in-email-otp"
|
||||
"reference/vue/use-sign-in-email-otp",
|
||||
"reference/vue/use-sign-in-id-token",
|
||||
"reference/vue/use-link-id-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.22.0",
|
||||
"version": "2.24.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
@@ -21,7 +21,9 @@ Combined with a powerful **Permission Rules** system, Nhost Auth offers everythi
|
||||
</Card>
|
||||
<Card title="Security Keys (WebAuthn)" icon="square-5" href="../guides/auth/sign-in-webauthn">
|
||||
</Card>
|
||||
<Card title="Elevated Permissions" icon="square-6" href="../guides/auth/elevated-permissions">
|
||||
<Card title="ID Tokens" icon="square-6" href="../guides/auth/sign-in-idtokens">
|
||||
</Card>
|
||||
<Card title="Elevated Permissions" icon="square-7" href="../guides/auth/elevated-permissions">
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
28
docs/reference/javascript/auth/link-id-token.mdx
Normal file
28
docs/reference/javascript/auth/link-id-token.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: linkIdToken()
|
||||
sidebarTitle: linkIdToken()
|
||||
---
|
||||
|
||||
Use `nhost.auth.linkIdToken` to link a user account with the provider's account using an ID token
|
||||
|
||||
```ts
|
||||
nhost.auth.linkIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`LinkIdTokenParams`](/reference/javascript/auth/types/link-id-token-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :------------------------------------------------------------------------------------------ | :------------------------------------------------------ | :------: | :---- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>idToken</span> | <code>string</code> | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>provider</span> | [`Provider`](/reference/javascript/auth/types/provider) | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>nonce</span> | <code>string</code> | | |
|
||||
|
||||
---
|
||||
@@ -9,10 +9,10 @@ Note: The Nhost client automatically refreshes the session when the user is auth
|
||||
|
||||
```ts
|
||||
// Refresh the session with the the current internal refresh token.
|
||||
nhost.auth.refreshToken()
|
||||
nhost.auth.refreshSession()
|
||||
|
||||
// Refresh the session with an external refresh token.
|
||||
nhost.auth.refreshToken(refreshToken)
|
||||
nhost.auth.refreshSession(refreshToken)
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
28
docs/reference/javascript/auth/sign-in-id-token.mdx
Normal file
28
docs/reference/javascript/auth/sign-in-id-token.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: signInIdToken()
|
||||
sidebarTitle: signInIdToken()
|
||||
---
|
||||
|
||||
Use `nhost.auth.signInIdToken` to sign in a user with the provider's account using an ID token
|
||||
|
||||
```ts
|
||||
nhost.auth.signInIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`SignInIdTokenParams`](/reference/javascript/auth/types/sign-in-id-token-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :------------------------------------------------------------------------------------------ | :------------------------------------------------------ | :------: | :---- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>idToken</span> | <code>string</code> | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>provider</span> | [`Provider`](/reference/javascript/auth/types/provider) | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>nonce</span> | <code>string</code> | | |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: LinkIdTokenHandlerParams
|
||||
sidebarTitle: LinkIdTokenHandlerParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHandlerParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">idToken</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">provider</span>** <span className="optional-status">required</span> [`Provider`](/reference/javascript/auth/types/provider)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">nonce</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: LinkIdTokenHandlerResult
|
||||
sidebarTitle: LinkIdTokenHandlerResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHandlerResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/auth/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: LinkIdTokenParams
|
||||
sidebarTitle: LinkIdTokenParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenParams`
|
||||
|
||||
```ts
|
||||
type LinkIdTokenParams = () => { idToken: string, provider: [Provider](/reference/javascript/auth/types/provider), nonce: string }
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: SignInIdTokenHandlerParams
|
||||
sidebarTitle: SignInIdTokenHandlerParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHandlerParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">idToken</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">provider</span>** <span className="optional-status">required</span> [`Provider`](/reference/javascript/auth/types/provider)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">nonce</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: SignInIdTokenHandlerResult
|
||||
sidebarTitle: SignInIdTokenHandlerResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHandlerResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/auth/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/javascript/auth/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: SignInIdTokenParams
|
||||
sidebarTitle: SignInIdTokenParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenParams`
|
||||
|
||||
```ts
|
||||
type SignInIdTokenParams = () => { idToken: string, provider: [Provider](/reference/javascript/auth/types/provider), nonce: string }
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: SignInIdTokenState
|
||||
sidebarTitle: SignInIdTokenState
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenState`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/auth/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isLoading</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` when the action is executing, `false` when it finished its execution.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/javascript/auth/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
28
docs/reference/javascript/nhost-js/link-id-token.mdx
Normal file
28
docs/reference/javascript/nhost-js/link-id-token.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: linkIdToken()
|
||||
sidebarTitle: linkIdToken()
|
||||
---
|
||||
|
||||
Use `nhost.auth.linkIdToken` to link a user account with the provider's account using an ID token
|
||||
|
||||
```ts
|
||||
nhost.auth.linkIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`LinkIdTokenParams`](/reference/javascript/nhost-js/types/link-id-token-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :------------------------------------------------------------------------------------------ | :---------------------------------------------------------- | :------: | :---- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>idToken</span> | <code>string</code> | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>provider</span> | [`Provider`](/reference/javascript/nhost-js/types/provider) | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>nonce</span> | <code>string</code> | | |
|
||||
|
||||
---
|
||||
28
docs/reference/javascript/nhost-js/sign-in-id-token.mdx
Normal file
28
docs/reference/javascript/nhost-js/sign-in-id-token.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: signInIdToken()
|
||||
sidebarTitle: signInIdToken()
|
||||
---
|
||||
|
||||
Use `nhost.auth.signInIdToken` to sign in a user with the provider's account using an ID token
|
||||
|
||||
```ts
|
||||
nhost.auth.signInIdToken({
|
||||
provider: 'google', // The provider name, e.g., 'google', 'apple', etc.
|
||||
idToken: '...', // The ID token issued by the provider.
|
||||
nonce: '...' // Optional: The nonce used during token generation.
|
||||
})
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`SignInIdTokenParams`](/reference/javascript/nhost-js/types/sign-in-id-token-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :------------------------------------------------------------------------------------------ | :---------------------------------------------------------- | :------: | :---- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>idToken</span> | <code>string</code> | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>provider</span> | [`Provider`](/reference/javascript/nhost-js/types/provider) | ✔️ | |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>nonce</span> | <code>string</code> | | |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: LinkIdTokenHandlerParams
|
||||
sidebarTitle: LinkIdTokenHandlerParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHandlerParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">idToken</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">provider</span>** <span className="optional-status">required</span> [`Provider`](/reference/javascript/nhost-js/types/provider)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">nonce</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: LinkIdTokenHandlerResult
|
||||
sidebarTitle: LinkIdTokenHandlerResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHandlerResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/nhost-js/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: LinkIdTokenParams
|
||||
sidebarTitle: LinkIdTokenParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenParams`
|
||||
|
||||
```ts
|
||||
type LinkIdTokenParams = () => { idToken: string, provider: [Provider](/reference/javascript/nhost-js/types/provider), nonce: string }
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: SignInIdTokenHandlerParams
|
||||
sidebarTitle: SignInIdTokenHandlerParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHandlerParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">idToken</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">provider</span>** <span className="optional-status">required</span> [`Provider`](/reference/javascript/nhost-js/types/provider)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">nonce</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
title: SignInIdTokenHandlerResult
|
||||
sidebarTitle: SignInIdTokenHandlerResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHandlerResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/nhost-js/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/javascript/nhost-js/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: SignInIdTokenParams
|
||||
sidebarTitle: SignInIdTokenParams
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenParams`
|
||||
|
||||
```ts
|
||||
type SignInIdTokenParams = () => { idToken: string, provider: [Provider](/reference/javascript/nhost-js/types/provider), nonce: string }
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: SignInIdTokenState
|
||||
sidebarTitle: SignInIdTokenState
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenState`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`AuthErrorPayload`](/reference/javascript/nhost-js/types/auth-error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isLoading</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` when the action is executing, `false` when it finished its execution.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/javascript/nhost-js/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
39
docs/reference/nextjs/types/link-id-token-hook-result.mdx
Normal file
39
docs/reference/nextjs/types/link-id-token-hook-result.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: LinkIdTokenHookResult
|
||||
sidebarTitle: LinkIdTokenHookResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHookResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`ErrorPayload`](/reference/nextjs/types/error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">linkIdToken</span>** <span className="optional-status">required</span> <code>LinkIdTokenHandler</code>
|
||||
|
||||
---
|
||||
65
docs/reference/nextjs/types/sign-in-id-token-hook-result.mdx
Normal file
65
docs/reference/nextjs/types/sign-in-id-token-hook-result.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: SignInIdTokenHookResult
|
||||
sidebarTitle: SignInIdTokenHookResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHookResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | AuthErrorPayload</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isLoading</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` when the action is executing, `false` when it finished its execution.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/nextjs/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">signInIdToken</span>** <span className="optional-status">required</span> <code>SignInIdTokenHandler</code>
|
||||
|
||||
---
|
||||
20
docs/reference/nextjs/use-link-id-token.mdx
Normal file
20
docs/reference/nextjs/use-link-id-token.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: useLinkIdToken()
|
||||
sidebarTitle: useLinkIdToken()
|
||||
---
|
||||
|
||||
Use the hook `useLinkIdToken` to link a user account with the provider's account using an id token
|
||||
|
||||
```tsx
|
||||
const { linkIdToken, isLoading, isSuccess, isError, error } = useLinkIdToken()
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
await linkIdToken({
|
||||
provider: 'google',
|
||||
idToken: '...',
|
||||
nonce: '...'
|
||||
})
|
||||
}
|
||||
```
|
||||
19
docs/reference/nextjs/use-sign-in-id-token.mdx
Normal file
19
docs/reference/nextjs/use-sign-in-id-token.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: useSignInIdToken()
|
||||
sidebarTitle: useSignInIdToken()
|
||||
---
|
||||
|
||||
Use the hook `useSignInIdToken` to sign in a user using an `idToken`.
|
||||
|
||||
```tsx
|
||||
const { signInIdToken, isLoading, isSuccess, isError, error } =
|
||||
useSignInIdToken()
|
||||
|
||||
console.log({ isLoading, isSuccess, isError, error })
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
await signInIdToken({ provider: 'google', idToken: '...' })
|
||||
}
|
||||
```
|
||||
39
docs/reference/react/types/link-id-token-hook-result.mdx
Normal file
39
docs/reference/react/types/link-id-token-hook-result.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: LinkIdTokenHookResult
|
||||
sidebarTitle: LinkIdTokenHookResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenHookResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | [`ErrorPayload`](/reference/react/types/error-payload)</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">linkIdToken</span>** <span className="optional-status">required</span> <code>LinkIdTokenHandler</code>
|
||||
|
||||
---
|
||||
65
docs/reference/react/types/sign-in-id-token-hook-result.mdx
Normal file
65
docs/reference/react/types/sign-in-id-token-hook-result.mdx
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
title: SignInIdTokenHookResult
|
||||
sidebarTitle: SignInIdTokenHookResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `SignInIdTokenHookResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>null | AuthErrorPayload</code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isLoading</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` when the action is executing, `false` when it finished its execution.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>boolean</code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">user</span>** <span className="optional-status">required</span> <code>null | [`User`](/reference/react/types/user)</code>
|
||||
|
||||
User information
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">refreshToken</span>** <span className="optional-status">required</span> <code>null | string</code>
|
||||
|
||||
Access token (JWT)
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">signInIdToken</span>** <span className="optional-status">required</span> <code>SignInIdTokenHandler</code>
|
||||
|
||||
---
|
||||
20
docs/reference/react/use-link-id-token.mdx
Normal file
20
docs/reference/react/use-link-id-token.mdx
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: useLinkIdToken()
|
||||
sidebarTitle: useLinkIdToken()
|
||||
---
|
||||
|
||||
Use the hook `useLinkIdToken` to link a user account with the provider's account using an id token
|
||||
|
||||
```tsx
|
||||
const { linkIdToken, isLoading, isSuccess, isError, error } = useLinkIdToken()
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
await linkIdToken({
|
||||
provider: 'google',
|
||||
idToken: '...',
|
||||
nonce: '...'
|
||||
})
|
||||
}
|
||||
```
|
||||
19
docs/reference/react/use-sign-in-id-token.mdx
Normal file
19
docs/reference/react/use-sign-in-id-token.mdx
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: useSignInIdToken()
|
||||
sidebarTitle: useSignInIdToken()
|
||||
---
|
||||
|
||||
Use the hook `useSignInIdToken` to sign in a user using an `idToken`.
|
||||
|
||||
```tsx
|
||||
const { signInIdToken, isLoading, isSuccess, isError, error } =
|
||||
useSignInIdToken()
|
||||
|
||||
console.log({ isLoading, isSuccess, isError, error })
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
await signInIdToken({ provider: 'google', idToken: '...' })
|
||||
}
|
||||
```
|
||||
47
docs/reference/vue/types/link-id-token-result.mdx
Normal file
47
docs/reference/vue/types/link-id-token-result.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: LinkIdTokenResult
|
||||
sidebarTitle: LinkIdTokenResult
|
||||
description: No description provided.
|
||||
---
|
||||
|
||||
# `LinkIdTokenResult`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isError</span>** <span className="optional-status">required</span> <code>Ref<boolean></code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` if an error occurred
|
||||
|
||||
**`@depreacted`**
|
||||
|
||||
use `!isSuccess` or `!!error` instead
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">error</span>** <span className="optional-status">required</span> <code>Ref<null | ErrorPayload<any>></code>
|
||||
|
||||
Provides details about the error
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isLoading</span>** <span className="optional-status">required</span> <code>Ref<boolean></code>
|
||||
|
||||
**`@returns`**
|
||||
|
||||
`true` when the action is executing, `false` when it finished its execution.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">isSuccess</span>** <span className="optional-status">required</span> <code>Ref<boolean></code>
|
||||
|
||||
Returns `true` if the action is successful.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">linkIdToken</span>** <span className="optional-status">required</span> <code>(params: LinkIdTokenHandlerParams) => Promise<LinkIdTokenHandlerResult></code>
|
||||
|
||||
---
|
||||
18
docs/reference/vue/use-link-id-token.mdx
Normal file
18
docs/reference/vue/use-link-id-token.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: useLinkIdToken()
|
||||
sidebarTitle: useLinkIdToken()
|
||||
---
|
||||
|
||||
Use the hook `useLinkIdToken` to link a user account with the provider's account using an id token
|
||||
|
||||
```tsx
|
||||
const { linkIdToken, isLoading, isSuccess, isError, error } = useLinkIdToken()
|
||||
|
||||
const handleLinkIdToken = async () => {
|
||||
await linkIdToken({
|
||||
provider: 'google',
|
||||
idToken: '...',
|
||||
nonce: '...'
|
||||
})
|
||||
}
|
||||
```
|
||||
17
docs/reference/vue/use-sign-in-id-token.mdx
Normal file
17
docs/reference/vue/use-sign-in-id-token.mdx
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: useSignInIdToken()
|
||||
sidebarTitle: useSignInIdToken()
|
||||
---
|
||||
|
||||
Use the composable `useSignInIdToken` to sign in a user using an `idToken`.
|
||||
|
||||
```ts
|
||||
const { signInIdToken, isLoading, isSuccess, isError, error } =
|
||||
useSignInIdToken()
|
||||
|
||||
console.log({ isLoading, isSuccess, isError, error })
|
||||
|
||||
const handleSignInIdToken = async () => {
|
||||
await signInIdToken({ provider: 'google', idToken: '...' })
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost-examples/cli
|
||||
|
||||
## 0.3.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@3.2.1
|
||||
|
||||
## 0.3.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user