Compare commits
23 Commits
@nhost/rea
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
760835d80f | ||
|
|
6a34f891a5 | ||
|
|
037bd74764 | ||
|
|
0f6ce52c4e | ||
|
|
6696172bcb | ||
|
|
b0e848d353 | ||
|
|
cea3ef5c8a | ||
|
|
a05db74bb6 | ||
|
|
73f3d69776 | ||
|
|
a99f034bd4 | ||
|
|
3b37af06a0 | ||
|
|
86ecf27b23 | ||
|
|
1b5dc5e7f5 | ||
|
|
21708be3d2 | ||
|
|
f16e2305c3 | ||
|
|
5d6c349350 | ||
|
|
245a1b44c4 | ||
|
|
ca75f731af | ||
|
|
c48be24d13 | ||
|
|
60b5bf20d7 | ||
|
|
8f94bc6332 | ||
|
|
75c73c4884 | ||
|
|
4c6a6bb6c1 |
@@ -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
|
||||
|
||||
@@ -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,30 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 2.11.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6a34f89: fix: improve project polling logic and unify usage across components
|
||||
|
||||
## 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.2",
|
||||
"version": "2.11.2",
|
||||
"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} />
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
||||
* @returns A function that returns a new ApolloClient instance.
|
||||
*/
|
||||
export default function useRemoteApplicationGQLClient() {
|
||||
const { project, loading } = useProject({ target: 'user-project' });
|
||||
const { project, loading } = useProject();
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function useGetAppUsers({
|
||||
offset = 0,
|
||||
options = {},
|
||||
}: UseFilesOptions) {
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||
const { data, error, loading } = useRemoteAppGetUsersCustomQuery({
|
||||
...options,
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function useAppClient(
|
||||
options?: UseAppClientOptions,
|
||||
): UseAppClientReturn {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
|
||||
if (!isPlatform) {
|
||||
return new NhostClient({
|
||||
|
||||
@@ -2,19 +2,18 @@ import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
GetProjectDocument,
|
||||
useGetProjectQuery,
|
||||
type GetProjectQuery,
|
||||
type ProjectFragment,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
type Project = GetProjectQuery['apps'][0];
|
||||
|
||||
interface UseProjectOptions {
|
||||
poll?: boolean;
|
||||
target?: 'console-next' | 'user-project';
|
||||
}
|
||||
|
||||
export interface UseProjectReturnType {
|
||||
@@ -26,7 +25,6 @@ export interface UseProjectReturnType {
|
||||
|
||||
export default function useProject({
|
||||
poll = false,
|
||||
target = 'console-next',
|
||||
}: UseProjectOptions = {}): UseProjectReturnType {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
@@ -37,65 +35,41 @@ export default function useProject({
|
||||
const { isAuthenticated, isLoading: isAuthLoading } =
|
||||
useAuthenticationStatus();
|
||||
|
||||
const shouldFetchProject =
|
||||
isPlatform &&
|
||||
isAuthenticated &&
|
||||
!isAuthLoading &&
|
||||
!!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,
|
||||
} = useQuery(
|
||||
['currentProject', appSubdomain],
|
||||
const shouldFetchProject = useMemo(
|
||||
() =>
|
||||
client.graphql.request<{ apps: ProjectFragment[] }>(GetProjectDocument, {
|
||||
isPlatform &&
|
||||
isAuthenticated &&
|
||||
!isAuthLoading &&
|
||||
!!appSubdomain &&
|
||||
isRouterReady,
|
||||
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
|
||||
);
|
||||
|
||||
const { data, isLoading, refetch, error } = useQuery(
|
||||
['currentProject', appSubdomain as string],
|
||||
async () => {
|
||||
const response = await client.graphql.request<{
|
||||
apps: ProjectFragment[];
|
||||
}>(GetProjectDocument, {
|
||||
subdomain: (appSubdomain as string) || '',
|
||||
}),
|
||||
});
|
||||
return response;
|
||||
},
|
||||
{
|
||||
enabled: shouldFetchProject,
|
||||
keepPreviousData: true,
|
||||
enabled: shouldFetchProject && target === 'user-project',
|
||||
staleTime: poll ? 5000 : Infinity, // Adjust staleTime for better performance
|
||||
refetchInterval: poll ? 10000 : false,
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 1000 * 60 * 5, // 1 minutes
|
||||
cacheTime: 1000 * 60 * 6, //
|
||||
},
|
||||
);
|
||||
|
||||
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: isLoading && shouldFetchProject,
|
||||
error: Array.isArray(error || {}) ? error[0] : error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
||||
value: { fetchBlob, id, mimeType, alt, blob },
|
||||
fallbackPreview = null,
|
||||
}: DataGridPreviewCellProps<TData>) {
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const appClient = useAppClient();
|
||||
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
@@ -13,10 +13,10 @@ import { FilesDataGridControls } from '@/features/orgs/projects/storage/dataGrid
|
||||
import { useBuckets } from '@/features/orgs/projects/storage/dataGrid/hooks/useBuckets';
|
||||
import { useFiles } from '@/features/orgs/projects/storage/dataGrid/hooks/useFiles';
|
||||
import { useFilesAggregate } from '@/features/orgs/projects/storage/dataGrid/hooks/useFilesAggregate';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import type { Files } from '@/utils/__generated__/graphql';
|
||||
import { Order_By as OrderBy } from '@/utils/__generated__/graphql';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ChangeEvent } from 'react';
|
||||
@@ -32,7 +32,7 @@ export type FilesDataGridProps = Partial<DataGridProps<StoredFile>>;
|
||||
|
||||
export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
const router = useRouter();
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const appClient = useAppClient();
|
||||
const [searchString, setSearchString] = useState<string | null>(null);
|
||||
const [currentOffset, setCurrentOffset] = useState<number | null>(
|
||||
@@ -118,7 +118,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
DataGridPreviewCell({
|
||||
...cellProps,
|
||||
fallbackPreview: (
|
||||
<FilePreviewIcon className="w-5 h-5 fill-current" />
|
||||
<FilePreviewIcon className="h-5 w-5 fill-current" />
|
||||
),
|
||||
}),
|
||||
minWidth: 80,
|
||||
|
||||
@@ -12,9 +12,9 @@ import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
|
||||
import { FileUploadButton } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
|
||||
import type { Files } from '@/utils/__generated__/graphql';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type { Files } from '@/utils/__generated__/graphql';
|
||||
import type { PropsWithoutRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { Row } from 'react-table';
|
||||
@@ -38,7 +38,7 @@ export default function FilesDataGridControls({
|
||||
...props
|
||||
}: FilesDataGridControlsProps) {
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const appClient = useAppClient();
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function FilesDataGridControls({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid w-full grid-cols-12 gap-2 mx-auto">
|
||||
<div className="mx-auto grid w-full grid-cols-12 gap-2">
|
||||
<Input
|
||||
className={twMerge(
|
||||
'col-span-12 xs+:col-span-12 md:col-span-9 xl:col-span-10',
|
||||
@@ -170,7 +170,7 @@ export default function FilesDataGridControls({
|
||||
{...restFilterProps}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-col col-span-12 gap-2 md:col-span-3 xl:col-span-2">
|
||||
<div className="col-span-12 grid grid-flow-col gap-2 md:col-span-3 xl:col-span-2">
|
||||
<DataGridPagination
|
||||
className={twMerge('col-span-6', paginationClassName)}
|
||||
{...restPaginationProps}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import type {
|
||||
Files_Order_By as FilesOrderBy,
|
||||
GetFilesQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useGetFilesQuery } from '@/utils/__generated__/graphql';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import type { QueryHookOptions } from '@apollo/client';
|
||||
|
||||
export type UseFilesOptions = {
|
||||
@@ -38,7 +38,7 @@ export default function useFiles({
|
||||
orderBy,
|
||||
options = {},
|
||||
}: UseFilesOptions) {
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const { data, previousData, ...rest } = useGetFilesQuery({
|
||||
variables: {
|
||||
where: searchString
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
};
|
||||
@@ -117,7 +117,7 @@ function GraphiQLHeader({ onUserChange, onRoleChange }: GraphiQLHeaderProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="grid items-end grid-flow-row gap-2 p-2 md:grid-flow-col md:justify-between">
|
||||
<header className="grid grid-flow-row items-end gap-2 p-2 md:grid-flow-col md:justify-between">
|
||||
<div className="grid grid-flow-row gap-2 md:grid-flow-col md:items-end">
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-flow-col md:grid-cols-[initial]">
|
||||
<UserSelect
|
||||
@@ -250,7 +250,7 @@ function GraphiQLEditor({ onHeaderChange }: GraphiQLEditorProps) {
|
||||
}
|
||||
|
||||
export default function GraphQLPage() {
|
||||
const { project } = useProject({ target: 'user-project' });
|
||||
const { project } = useProject();
|
||||
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
|
||||
|
||||
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
|
||||
|
||||
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,11 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.24.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a99f034: chore: fix function name
|
||||
|
||||
## 2.23.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") |
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.23.0",
|
||||
"version": "2.24.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/nhost-js": "^3.1.5",
|
||||
"@playwright/test": "^1.41.0",
|
||||
"@sveltejs/adapter-auto": "^2.1.1",
|
||||
"@sveltejs/kit": "^1.30.4",
|
||||
"@sveltejs/adapter-auto": "^3.3.1",
|
||||
"@sveltejs/kit": "^2.11.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -25,15 +25,16 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte": "^5.14.0",
|
||||
"svelte-check": "^3.6.8",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^5.4.6",
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^0.25.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@nhost/nhost-js": "^3.2.1",
|
||||
"graphql": "16.8.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
|
||||
205
examples/quickstarts/sveltekit/pnpm-lock.yaml
generated
205
examples/quickstarts/sveltekit/pnpm-lock.yaml
generated
@@ -1,205 +0,0 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
graphql:
|
||||
specifier: 16.8.1
|
||||
version: 16.8.1
|
||||
|
||||
devDependencies:
|
||||
'@nhost/nhost-js':
|
||||
specifier: ^3.1.5
|
||||
version: 3.1.5(graphql@16.8.1)
|
||||
|
||||
packages:
|
||||
|
||||
/@graphql-typed-document-node/core@3.2.0(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
|
||||
peerDependencies:
|
||||
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
|
||||
dependencies:
|
||||
graphql: 16.8.1
|
||||
dev: true
|
||||
|
||||
/@nhost/graphql-js@0.3.0(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-CVYq6wx0VbaYdpUBmfNO/6mZatHB5+YBCqFjWyxhpN1nzHCHEO6rgdL7j0qk31OFE6XAX0z7AQZSXg1Pn63GUw==}
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
'@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1)
|
||||
base-64: 1.0.0
|
||||
graphql: 16.8.1
|
||||
isomorphic-unfetch: 3.1.0
|
||||
jwt-decode: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-auth-js@2.5.2:
|
||||
resolution: {integrity: sha512-3O4fIJ8xbdCdKGR/1o5jMczxrLLQ2g6BNp6J9m83COVqg9ka5IXoFuM6pgbX5W7WPe9nIQntvHsfeDynXS+/fg==}
|
||||
dependencies:
|
||||
'@simplewebauthn/browser': 9.0.1
|
||||
fetch-ponyfill: 7.1.0
|
||||
js-cookie: 3.0.5
|
||||
jwt-decode: 4.0.0
|
||||
xstate: 4.38.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/hasura-storage-js@2.5.1:
|
||||
resolution: {integrity: sha512-I3rOSa095lcR9BUmNw7dOoXLPWL39WOcrb0paUBFX4h3ltR92ILEHTZ38hN6bZSv157ZdqkIFNL/M2G45SSf7g==}
|
||||
dependencies:
|
||||
fetch-ponyfill: 7.1.0
|
||||
form-data: 4.0.0
|
||||
graphql: 16.8.1
|
||||
xstate: 4.38.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@nhost/nhost-js@3.1.5(graphql@16.8.1):
|
||||
resolution: {integrity: sha512-SgDGQ0APiRPc6RB2Cl1EcvQUsmWFDx32JmgqYBgLKKV9+PBDKc2GJ4GHECbxQi3RoIMXeJ777XJTEK7mGgNL+A==}
|
||||
peerDependencies:
|
||||
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
'@nhost/graphql-js': 0.3.0(graphql@16.8.1)
|
||||
'@nhost/hasura-auth-js': 2.5.2
|
||||
'@nhost/hasura-storage-js': 2.5.1
|
||||
graphql: 16.8.1
|
||||
isomorphic-unfetch: 3.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/@simplewebauthn/browser@9.0.1:
|
||||
resolution: {integrity: sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==}
|
||||
dependencies:
|
||||
'@simplewebauthn/types': 9.0.1
|
||||
dev: true
|
||||
|
||||
/@simplewebauthn/types@9.0.1:
|
||||
resolution: {integrity: sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==}
|
||||
dev: true
|
||||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/base-64@1.0.0:
|
||||
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
|
||||
dev: true
|
||||
|
||||
/combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/fetch-ponyfill@7.1.0:
|
||||
resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
|
||||
dependencies:
|
||||
node-fetch: 2.6.13
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/graphql@16.8.1:
|
||||
resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
/isomorphic-unfetch@3.1.0:
|
||||
resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==}
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
unfetch: 4.2.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: true
|
||||
|
||||
/js-cookie@3.0.5:
|
||||
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/jwt-decode@4.0.0:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
dev: true
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.6.13:
|
||||
resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: true
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: true
|
||||
|
||||
/unfetch@4.2.0:
|
||||
resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==}
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: true
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: true
|
||||
|
||||
/xstate@4.38.3:
|
||||
resolution: {integrity: sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==}
|
||||
dev: true
|
||||
@@ -3,7 +3,18 @@ import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
optimizeDeps: {
|
||||
include: ['@nhost/nhos-js']
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: {
|
||||
include: [/@nhost\/nhos-js/, /node_modules/]
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000
|
||||
},
|
||||
resolve: {
|
||||
preserveSymlinks: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -18,20 +18,15 @@ test('should be able to change email', async ({ page, browser }) => {
|
||||
|
||||
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
||||
|
||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Change emailChange$/ })
|
||||
.getByRole('button')
|
||||
.click()
|
||||
|
||||
// await expect(
|
||||
// newPage.getByText(/please check your inbox and follow the link to confirm the email change./i)
|
||||
// ).toBeVisible()
|
||||
|
||||
await expect(newPage.getByRole('status')).toContainText(
|
||||
'Please check your inbox and follow the link to confirm the email change.'
|
||||
)
|
||||
await expect(
|
||||
newPage.getByText('Please check your inbox and follow the link to confirm the email change.')
|
||||
).toBeVisible()
|
||||
|
||||
await newPage.getByRole('link', { name: /sign out/i }).click()
|
||||
|
||||
@@ -45,7 +40,6 @@ test('should be able to change email', async ({ page, browser }) => {
|
||||
requestType: 'email-confirm-change'
|
||||
})
|
||||
|
||||
// await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
|
||||
await expect(updatedEmailPage.getByRole('heading', { name: /profile/i })).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -64,7 +58,6 @@ test('should not accept an invalid email', async ({ page }) => {
|
||||
const newEmail = faker.random.alphaNumeric()
|
||||
|
||||
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
||||
|
||||
await newPage
|
||||
.locator('div')
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"eslint": "^8.57.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"pnpm": "^7.33.7",
|
||||
"typescript": "^4.9.5",
|
||||
"unocss": "^0.33.5",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
|
||||
10
package.json
10
package.json
@@ -20,7 +20,8 @@
|
||||
"build:dashboard": "turbo run build --filter=@nhost/dashboard",
|
||||
"build:docs": "turbo run build --filter=@nhost/docs",
|
||||
"build:all": "turbo run build --include-dependencies",
|
||||
"build:nextjs-server-components": "turbo run build --filter=@nhost-examples/nextjs-server-components",
|
||||
"build:@nhost-examples/nextjs-server-components": "turbo run build --filter=@nhost-examples/nextjs-server-components",
|
||||
"build:@nhost-examples/sveltekit": "turbo run build --filter=@nhost-examples/sveltekit",
|
||||
"dev": "turbo run dev --filter=!@nhost/dashboard --filter=!@nhost/docs --filter=!@nhost-examples/* --filter=!@nhost/docgen --no-deps --include-dependencies",
|
||||
"clean:all": "pnpm clean && rm -rf ./{{packages,examples/**,templates/**}/*,docs,dashboard}/{.nhost,node_modules} node_modules",
|
||||
"clean": "rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
|
||||
@@ -79,7 +80,7 @@
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"husky": "^8.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "^3.3.3",
|
||||
"turbo": "1.11.3",
|
||||
"typedoc": "^0.22.18",
|
||||
"typescript": "4.9.5",
|
||||
@@ -91,10 +92,9 @@
|
||||
"resolutions": {
|
||||
"graphql": "16.8.1"
|
||||
},
|
||||
"packageManager": "pnpm@8.10.5",
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"engines": {
|
||||
"node": ">=18 <19",
|
||||
"pnpm": ">=8.0.0"
|
||||
"node": ">=20"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "./config/.eslintrc.js"
|
||||
|
||||
@@ -804,10 +804,10 @@ export class HasuraAuthClient {
|
||||
* @example
|
||||
* ```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);
|
||||
* ```
|
||||
*
|
||||
* @docs https://docs.nhost.io/reference/javascript/auth/refresh-session
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"types": [],
|
||||
}
|
||||
}
|
||||
41872
pnpm-lock.yaml
generated
41872
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,11 @@
|
||||
"outputs": ["dist/**"],
|
||||
"env": ["VITE_NHOST_SUBDOMAIN", "VITE_NHOST_REGION"]
|
||||
},
|
||||
"@nhost-examples/sveltekit#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [".svelte-kit/**", ".vercel/**"],
|
||||
"env": ["PUBLIC_NHOST_SUBDOMAIN", "PUBLIC_NHOST_REGION"]
|
||||
},
|
||||
"@nhost-examples/vue-apollo#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"],
|
||||
|
||||
Reference in New Issue
Block a user