Compare commits
25 Commits
@nhost/rea
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c87736eeeb | ||
|
|
714dffa5ec | ||
|
|
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:
|
steps:
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: 8.10.5
|
version: 9.15.0
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Get pnpm cache directory
|
- name: Get pnpm cache directory
|
||||||
id: pnpm-cache-dir
|
id: pnpm-cache-dir
|
||||||
@@ -26,10 +26,10 @@ runs:
|
|||||||
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
restore-keys: ${{ runner.os }}-node-
|
restore-keys: ${{ runner.os }}-node-
|
||||||
- name: Use Node.js v18
|
- name: Use Node.js v20
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
- shell: bash
|
- shell: bash
|
||||||
name: Install packages
|
name: Install packages
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
26
.github/workflows/changesets.yaml
vendored
26
.github/workflows/changesets.yaml
vendored
@@ -65,29 +65,13 @@ jobs:
|
|||||||
|
|
||||||
publish-vercel:
|
publish-vercel:
|
||||||
name: Publish to Vercel
|
name: Publish to Vercel
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
needs:
|
||||||
- test
|
- test
|
||||||
steps:
|
uses: ./.github/workflows/deploy-dashboard.yaml
|
||||||
- name: Checkout repository
|
with:
|
||||||
uses: actions/checkout@v3
|
git_ref: ${{ github.ref_name }}
|
||||||
with:
|
environment: production
|
||||||
fetch-depth: 0
|
secrets: inherit
|
||||||
- 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 }}
|
|
||||||
|
|
||||||
publish-docker:
|
publish-docker:
|
||||||
name: Publish to Docker Hub
|
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
|
TURBO_TEAM: nhost
|
||||||
NEXT_PUBLIC_ENV: dev
|
NEXT_PUBLIC_ENV: dev
|
||||||
NEXT_TELEMETRY_DISABLED: 1
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
|
||||||
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
NHOST_TEST_DASHBOARD_URL: ${{ vars.NHOST_TEST_DASHBOARD_URL }}
|
||||||
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
NHOST_TEST_WORKSPACE_NAME: ${{ vars.NHOST_TEST_WORKSPACE_NAME }}
|
||||||
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_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
|
TURBO_TEAM: nhost
|
||||||
NEXT_PUBLIC_ENV: dev
|
NEXT_PUBLIC_ENV: dev
|
||||||
NEXT_TELEMETRY_DISABLED: 1
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
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:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
|
||||||
name: Run pr agent on every pull request, respond to user comments
|
name: Run pr agent on every pull request, respond to user comments
|
||||||
steps:
|
steps:
|
||||||
- name: PR Agent action step
|
- name: PR Agent action step
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ module.exports = {
|
|||||||
env: (config) => ({
|
env: (config) => ({
|
||||||
...config,
|
...config,
|
||||||
NEXT_PUBLIC_ENV: 'dev',
|
NEXT_PUBLIC_ENV: 'dev',
|
||||||
NEXT_PUBLIC_NHOST_BACKEND_URL: 'http://localhost:1337',
|
|
||||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
# @nhost/dashboard
|
# @nhost/dashboard
|
||||||
|
|
||||||
|
## 2.11.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 714dffa: fix: improve project polling logic and unify usage across components
|
||||||
|
|
||||||
|
## 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
|
## 1.30.0
|
||||||
|
|
||||||
### Minor Changes
|
### 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_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||||
ENV NEXT_PUBLIC_NHOST_CONFIGSERVER_URL __NEXT_PUBLIC_NHOST_CONFIGSERVER_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 .gitignore .gitignore
|
||||||
COPY --from=pruner /app/out/json/ .
|
COPY --from=pruner /app/out/json/ .
|
||||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ pnpm storybook --port 6007
|
|||||||
|
|
||||||
| Name | Description |
|
| 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_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_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`. |
|
| `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",
|
"name": "@nhost/dashboard",
|
||||||
"version": "2.7.2",
|
"version": "2.11.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"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;
|
href: string;
|
||||||
/**
|
/**
|
||||||
* Determines whether or not the link should be active if it's href exactly
|
* Determines whether or not the link should be active if href matches the current route.
|
||||||
* matches the current route.
|
|
||||||
*
|
*
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
@@ -87,7 +86,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
|||||||
<>
|
<>
|
||||||
<Backdrop
|
<Backdrop
|
||||||
open={expanded}
|
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"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={() => setExpanded(false)}
|
onClick={() => setExpanded(false)}
|
||||||
@@ -104,7 +103,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
|||||||
<Box
|
<Box
|
||||||
component="aside"
|
component="aside"
|
||||||
className={twMerge(
|
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',
|
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -119,6 +118,7 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
|||||||
>
|
>
|
||||||
Auto-Embeddings
|
Auto-Embeddings
|
||||||
</AINavLink>
|
</AINavLink>
|
||||||
|
|
||||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||||
Assistants
|
Assistants
|
||||||
</AINavLink>
|
</AINavLink>
|
||||||
|
|||||||
@@ -8,20 +8,15 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from '@/components/ui/v3/command';
|
} from '@/components/ui/v3/command';
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from '@/components/ui/v3/hover-card';
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/v3/popover';
|
} from '@/components/ui/v3/popover';
|
||||||
|
import { ProjectStatusIndicator } from '@/features/orgs/components/common/ProjectStatusIndicator';
|
||||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
|
||||||
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
import { Box, Check, ChevronsUpDown } from 'lucide-react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -31,56 +26,6 @@ type Option = {
|
|||||||
label: string;
|
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() {
|
export default function ProjectsComboBox() {
|
||||||
const {
|
const {
|
||||||
query: { appSubdomain },
|
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 { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
|
||||||
import { Form } from '@/components/form/Form';
|
import { Form } from '@/components/form/Form';
|
||||||
import { Box } from '@/components/ui/v2/Box';
|
import { Box } from '@/components/ui/v2/Box';
|
||||||
import { Button } from '@/components/ui/v2/Button';
|
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 { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||||
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
|
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
|
||||||
import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection';
|
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 type { DialogFormProps } from '@/types/common';
|
||||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
|
||||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
|
||||||
import {
|
import {
|
||||||
useInsertAssistantMutation,
|
useInsertAssistantMutation,
|
||||||
useUpdateAssistantMutation,
|
useUpdateAssistantMutation,
|
||||||
} from '@/utils/__generated__/graphite.graphql';
|
} 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 { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -28,6 +29,7 @@ export const validationSchema = Yup.object({
|
|||||||
description: Yup.string(),
|
description: Yup.string(),
|
||||||
instructions: Yup.string().required('The instructions are required'),
|
instructions: Yup.string().required('The instructions are required'),
|
||||||
model: Yup.string().required('The model is required'),
|
model: Yup.string().required('The model is required'),
|
||||||
|
fileStore: Yup.string().label('File Store'),
|
||||||
graphql: Yup.array().of(
|
graphql: Yup.array().of(
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
name: Yup.string().required(),
|
name: Yup.string().required(),
|
||||||
@@ -64,14 +66,14 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
|||||||
|
|
||||||
export interface AssistantFormProps extends DialogFormProps {
|
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;
|
assistantId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if there is initialData then it's an update operation
|
* if there is initialData then it's an update operation
|
||||||
*/
|
*/
|
||||||
initialData?: AssistantFormValues;
|
initialData?: AssistantFormValues
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to be called when the operation is cancelled.
|
* Function to be called when the operation is cancelled.
|
||||||
@@ -114,26 +116,26 @@ export default function AssistantForm({
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, location);
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, location, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
const createOrUpdateAutoEmbeddings = async (
|
const createOrUpdateAssistant = async (
|
||||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
values: DeepRequired<AssistantFormValues> & {
|
||||||
|
assistantID: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
// remove any __typename from the form values
|
// remove any __typename from the form values
|
||||||
const payload = removeTypename(values);
|
const payload = removeTypename(values);
|
||||||
|
|
||||||
if (values.webhooks.length === 0) {
|
if (values.webhooks?.length === 0) {
|
||||||
delete payload.webhooks;
|
delete payload.webhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.graphql.length === 0) {
|
if (values.graphql?.length === 0) {
|
||||||
delete payload.graphql;
|
delete payload.graphql;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove assistantId because the update mutation fails otherwise
|
|
||||||
delete payload.assistantID;
|
delete payload.assistantID;
|
||||||
|
|
||||||
// If the assistantId is set then we do an update
|
// If the assistantId is set then we do an update
|
||||||
@@ -158,11 +160,13 @@ export default function AssistantForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (
|
const handleSubmit = async (
|
||||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
values: DeepRequired<AssistantFormValues> & {
|
||||||
|
assistantID: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
await execPromiseWithErrorToast(
|
await execPromiseWithErrorToast(
|
||||||
async () => {
|
async () => {
|
||||||
await createOrUpdateAutoEmbeddings(values);
|
await createOrUpdateAssistant(values);
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -282,6 +286,7 @@ export default function AssistantForm({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GraphqlDataSourcesFormSection />
|
<GraphqlDataSourcesFormSection />
|
||||||
<WebhooksDataSourcesFormSection />
|
<WebhooksDataSourcesFormSection />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
|
|||||||
|
|
||||||
interface AssistantsListProps {
|
interface AssistantsListProps {
|
||||||
/**
|
/**
|
||||||
* The run services fetched from entering the users page.
|
* The list of assistants.
|
||||||
*/
|
*/
|
||||||
assistants: Assistant[];
|
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()}
|
* @example onDelete={() => refetch()}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ export default function Estimate() {
|
|||||||
|
|
||||||
const amountDue = useMemo(() => {
|
const amountDue = useMemo(() => {
|
||||||
const amount = data?.billingGetNextInvoice?.AmountDue;
|
const amount = data?.billingGetNextInvoice?.AmountDue;
|
||||||
if (!amount) {
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
if (typeof amount !== 'number') {
|
if (typeof amount !== 'number') {
|
||||||
return 'N/A';
|
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 { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||||
import { Input } from '@/components/ui/v2/Input';
|
import { Input } from '@/components/ui/v2/Input';
|
||||||
import { Button } from '@/components/ui/v3/button';
|
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 { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||||
import { DeploymentStatusMessage } from '@/features/projects/deployments/components/DeploymentStatusMessage';
|
|
||||||
import {
|
import {
|
||||||
useGetProjectsQuery,
|
useGetProjectsQuery,
|
||||||
type GetProjectsQuery,
|
type GetProjectsQuery,
|
||||||
@@ -22,20 +23,21 @@ function ProjectCard({ project }: { project: Project }) {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/orgs/${org?.slug}/projects/${project.subdomain}`}
|
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 flex-row items-start gap-2">
|
||||||
<div className="flex w-full flex-row items-center space-x-2">
|
<Box className="mt-[2px] h-5 w-5 flex-shrink-0" />
|
||||||
<Box className="h-6 w-6 flex-shrink-0" />
|
<div className="flex w-full flex-col">
|
||||||
<p className="truncate text-lg font-bold">{project.name}</p>
|
<p className="truncate font-bold">{project.name}</p>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{project.region.name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ProjectStatusIndicator status={project.appStates[0].stateId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row items-start gap-2">
|
<div className="flex flex-1 flex-row items-start gap-2">
|
||||||
<DeploymentStatusMessage
|
<DeploymentStatusMessage deployment={latestDeployment} />
|
||||||
appCreatedAt={project.createdAt}
|
|
||||||
deployment={latestDeployment}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
@@ -53,6 +55,7 @@ export default function ProjectsGrid() {
|
|||||||
orgSlug: org?.slug,
|
orgSlug: org?.slug,
|
||||||
},
|
},
|
||||||
skip: !org,
|
skip: !org,
|
||||||
|
pollInterval: 10 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
@@ -100,7 +103,7 @@ export default function ProjectsGrid() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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) => (
|
{filteredProjects.map((project) => (
|
||||||
<ProjectCard key={project.id} project={project} />
|
<ProjectCard key={project.id} project={project} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||||||
* @returns A function that returns a new ApolloClient instance.
|
* @returns A function that returns a new ApolloClient instance.
|
||||||
*/
|
*/
|
||||||
export default function useRemoteApplicationGQLClient() {
|
export default function useRemoteApplicationGQLClient() {
|
||||||
const { project, loading } = useProject({ target: 'user-project' });
|
const { project, loading } = useProject();
|
||||||
const serviceUrl = generateAppServiceUrl(
|
const serviceUrl = generateAppServiceUrl(
|
||||||
project?.subdomain,
|
project?.subdomain,
|
||||||
project?.region,
|
project?.region,
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ export default function AISidebar({ className, ...props }: AISidebarProps) {
|
|||||||
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
<AINavLink href="/assistants" exact={false} onClick={handleSelect}>
|
||||||
Assistants
|
Assistants
|
||||||
</AINavLink>
|
</AINavLink>
|
||||||
|
<AINavLink href="/file-stores" exact={false} onClick={handleSelect}>
|
||||||
|
File Stores
|
||||||
|
</AINavLink>
|
||||||
</List>
|
</List>
|
||||||
</nav>
|
</nav>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ApplicationUnknown } from '@/features/orgs/projects/common/components/A
|
|||||||
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
||||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -37,7 +37,7 @@ function ProjectLayoutContent({
|
|||||||
|
|
||||||
const { state } = useAppState();
|
const { state } = useAppState();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { project, loading, error } = useProject({ poll: true });
|
const { project, loading, error } = useProjectWithState();
|
||||||
|
|
||||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||||
|
|
||||||
|
|||||||
@@ -14,21 +14,27 @@ import { WebhooksDataSourcesFormSection } from '@/features/orgs/projects/ai/Assi
|
|||||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||||
import type { DialogFormProps } from '@/types/common';
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
|
||||||
import {
|
import {
|
||||||
useInsertAssistantMutation,
|
useInsertAssistantMutation,
|
||||||
useUpdateAssistantMutation,
|
useUpdateAssistantMutation,
|
||||||
} from '@/utils/__generated__/graphite.graphql';
|
} from '@/utils/__generated__/graphite.graphql';
|
||||||
|
import { removeTypename, type DeepRequired } from '@/utils/helpers';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import * as Yup from 'yup';
|
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({
|
export const validationSchema = Yup.object({
|
||||||
name: Yup.string().required('The name is required.'),
|
name: Yup.string().required('The name is required.'),
|
||||||
description: Yup.string(),
|
description: Yup.string(),
|
||||||
instructions: Yup.string().required('The instructions are required'),
|
instructions: Yup.string().required('The instructions are required'),
|
||||||
model: Yup.string().required('The model is required'),
|
model: Yup.string().required('The model is required'),
|
||||||
|
fileStore: Yup.string().label('File Store'),
|
||||||
graphql: Yup.array().of(
|
graphql: Yup.array().of(
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
name: Yup.string().required(),
|
name: Yup.string().required(),
|
||||||
@@ -65,14 +71,17 @@ export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
|||||||
|
|
||||||
export interface AssistantFormProps extends DialogFormProps {
|
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;
|
assistantId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if there is initialData then it's an update operation
|
* 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.
|
* Function to be called when the operation is cancelled.
|
||||||
@@ -87,6 +96,7 @@ export interface AssistantFormProps extends DialogFormProps {
|
|||||||
export default function AssistantForm({
|
export default function AssistantForm({
|
||||||
assistantId,
|
assistantId,
|
||||||
initialData,
|
initialData,
|
||||||
|
fileStores,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
location,
|
location,
|
||||||
@@ -103,8 +113,27 @@ export default function AssistantForm({
|
|||||||
client: adminClient,
|
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>({
|
const form = useForm<AssistantFormValues>({
|
||||||
defaultValues: initialData,
|
defaultValues: formDefaultValues,
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
@@ -120,22 +149,32 @@ export default function AssistantForm({
|
|||||||
onDirtyStateChange(isDirty, location);
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, location, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
const createOrUpdateAutoEmbeddings = async (
|
const createOrUpdateAssistant = async (
|
||||||
values: DeepRequired<AssistantFormValues> & { assistantID: string },
|
values: DeepRequired<AssistantFormValues> & {
|
||||||
|
assistantID: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
// remove any __typename from the form values
|
// remove any __typename from the form values
|
||||||
const payload = removeTypename(values);
|
const payload = removeTypename(values);
|
||||||
|
|
||||||
if (values.webhooks.length === 0) {
|
if (values.webhooks?.length === 0) {
|
||||||
delete payload.webhooks;
|
delete payload.webhooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.graphql.length === 0) {
|
if (values.graphql?.length === 0) {
|
||||||
delete payload.graphql;
|
delete payload.graphql;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFileStoreSupported && values.fileStore) {
|
||||||
|
payload.fileStores = [values.fileStore];
|
||||||
|
}
|
||||||
|
if (!isFileStoreSupported) {
|
||||||
|
delete payload.fileStores;
|
||||||
|
}
|
||||||
|
|
||||||
// remove assistantId because the update mutation fails otherwise
|
// remove assistantId because the update mutation fails otherwise
|
||||||
delete payload.assistantID;
|
delete payload.assistantID;
|
||||||
|
delete payload.fileStore;
|
||||||
|
|
||||||
// If the assistantId is set then we do an update
|
// If the assistantId is set then we do an update
|
||||||
if (assistantId) {
|
if (assistantId) {
|
||||||
@@ -152,7 +191,7 @@ export default function AssistantForm({
|
|||||||
await insertAssistantMutation({
|
await insertAssistantMutation({
|
||||||
variables: {
|
variables: {
|
||||||
data: {
|
data: {
|
||||||
...values,
|
...payload,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -163,7 +202,7 @@ export default function AssistantForm({
|
|||||||
) => {
|
) => {
|
||||||
await execPromiseWithErrorToast(
|
await execPromiseWithErrorToast(
|
||||||
async () => {
|
async () => {
|
||||||
await createOrUpdateAutoEmbeddings(values);
|
await createOrUpdateAssistant(values);
|
||||||
onSubmit?.();
|
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 (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<Form
|
<Form
|
||||||
@@ -285,6 +328,36 @@ export default function AssistantForm({
|
|||||||
/>
|
/>
|
||||||
<GraphqlDataSourcesFormSection />
|
<GraphqlDataSourcesFormSection />
|
||||||
<WebhooksDataSourcesFormSection />
|
<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>
|
</div>
|
||||||
|
|
||||||
<Box className="flex flex-row justify-between w-full p-4 border-t rounded">
|
<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 { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||||
import { DeleteAssistantModal } from '@/features/orgs/projects/ai/DeleteAssistantModal';
|
import { DeleteAssistantModal } from '@/features/orgs/projects/ai/DeleteAssistantModal';
|
||||||
import { type Assistant } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/assistants';
|
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';
|
import { copy } from '@/utils/copy';
|
||||||
|
|
||||||
interface AssistantsListProps {
|
interface AssistantsListProps {
|
||||||
/**
|
/**
|
||||||
* The run services fetched from entering the users page.
|
* The list of assistants
|
||||||
*/
|
*/
|
||||||
assistants: Assistant[];
|
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()}
|
* @example onDelete={() => refetch()}
|
||||||
*/
|
*/
|
||||||
@@ -35,6 +41,7 @@ interface AssistantsListProps {
|
|||||||
|
|
||||||
export default function AssistantsList({
|
export default function AssistantsList({
|
||||||
assistants,
|
assistants,
|
||||||
|
fileStores,
|
||||||
onCreateOrUpdate,
|
onCreateOrUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: AssistantsListProps) {
|
}: AssistantsListProps) {
|
||||||
@@ -49,6 +56,7 @@ export default function AssistantsList({
|
|||||||
initialData={{
|
initialData={{
|
||||||
...assistant,
|
...assistant,
|
||||||
}}
|
}}
|
||||||
|
fileStores={fileStores}
|
||||||
onSubmit={() => onCreateOrUpdate()}
|
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';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,7 +9,7 @@ export default function useAppState(): {
|
|||||||
state: ApplicationStatus;
|
state: ApplicationStatus;
|
||||||
message?: string;
|
message?: string;
|
||||||
} {
|
} {
|
||||||
const { project } = useProject({ poll: true });
|
const { project } = useProjectWithState();
|
||||||
const noApplication = !project;
|
const noApplication = !project;
|
||||||
|
|
||||||
if (noApplication) {
|
if (noApplication) {
|
||||||
|
|||||||
@@ -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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||||
import { getHasuraAdminSecret } from '@/utils/env';
|
import { getHasuraAdminSecret } from '@/utils/env';
|
||||||
@@ -39,10 +39,12 @@ export default function useUpdateColumnMutation({
|
|||||||
const {
|
const {
|
||||||
query: { dataSourceSlug, schemaSlug, tableSlug },
|
query: { dataSourceSlug, schemaSlug, tableSlug },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
|
||||||
|
const { project } = useProject();
|
||||||
|
|
||||||
const appUrl = generateAppServiceUrl(
|
const appUrl = generateAppServiceUrl(
|
||||||
currentProject?.subdomain,
|
project?.subdomain,
|
||||||
currentProject?.region,
|
project?.region,
|
||||||
'hasura',
|
'hasura',
|
||||||
);
|
);
|
||||||
const mutationFn = isPlatform ? updateColumn : updateColumnMigration;
|
const mutationFn = isPlatform ? updateColumn : updateColumnMigration;
|
||||||
@@ -55,7 +57,7 @@ export default function useUpdateColumnMutation({
|
|||||||
adminSecret:
|
adminSecret:
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? getHasuraAdminSecret()
|
? getHasuraAdminSecret()
|
||||||
: customAdminSecret || currentProject?.config?.hasura.adminSecret,
|
: customAdminSecret || project?.config?.hasura.adminSecret,
|
||||||
dataSource: customDataSource || (dataSourceSlug as string),
|
dataSource: customDataSource || (dataSourceSlug as string),
|
||||||
schema: customSchema || (schemaSlug as string),
|
schema: customSchema || (schemaSlug as string),
|
||||||
table: customTable || (tableSlug 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 { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||||
import { getHasuraAdminSecret } from '@/utils/env';
|
import { getHasuraAdminSecret } from '@/utils/env';
|
||||||
import type { MutationOptions } from '@tanstack/react-query';
|
import type { MutationOptions } from '@tanstack/react-query';
|
||||||
@@ -40,10 +40,12 @@ export default function useUpdateRecordMutation<TData extends object = {}>({
|
|||||||
const {
|
const {
|
||||||
query: { dataSourceSlug, schemaSlug, tableSlug },
|
query: { dataSourceSlug, schemaSlug, tableSlug },
|
||||||
} = useRouter();
|
} = useRouter();
|
||||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
|
||||||
|
const { project } = useProject();
|
||||||
|
|
||||||
const appUrl = generateAppServiceUrl(
|
const appUrl = generateAppServiceUrl(
|
||||||
currentProject?.subdomain,
|
project?.subdomain,
|
||||||
currentProject?.region,
|
project?.region,
|
||||||
'hasura',
|
'hasura',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ export default function useUpdateRecordMutation<TData extends object = {}>({
|
|||||||
adminSecret:
|
adminSecret:
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? getHasuraAdminSecret()
|
? getHasuraAdminSecret()
|
||||||
: customAdminSecret || currentProject?.config?.hasura.adminSecret,
|
: customAdminSecret || project?.config?.hasura.adminSecret,
|
||||||
dataSource: customDataSource || (dataSourceSlug as string),
|
dataSource: customDataSource || (dataSourceSlug as string),
|
||||||
schema: customSchema || (schemaSlug as string),
|
schema: customSchema || (schemaSlug as string),
|
||||||
table: customTable || (tableSlug as string),
|
table: customTable || (tableSlug as string),
|
||||||
|
|||||||
@@ -23,30 +23,17 @@ afterAll(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render the avatar of the user who deployed the application', () => {
|
test('should render the avatar of the user who deployed the application', () => {
|
||||||
render(
|
render(<DeploymentStatusMessage deployment={defaultDeployment} />);
|
||||||
<DeploymentStatusMessage
|
|
||||||
deployment={defaultDeployment}
|
|
||||||
appCreatedAt="2023-02-24"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('img', {
|
screen.getByRole('img', {
|
||||||
name: `Avatar of ${defaultDeployment.commitUserName}`,
|
name: `Avatar of ${defaultDeployment.commitUserName}`,
|
||||||
}),
|
}),
|
||||||
).toHaveAttribute(
|
).toHaveAttribute('src', `${defaultDeployment.commitUserAvatarUrl}`);
|
||||||
'style',
|
|
||||||
`background-image: url(${defaultDeployment.commitUserAvatarUrl});`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render "updated just now" when the deployment is in progress and has not ended', () => {
|
test('should render "updated just now" when the deployment is in progress and has not ended', () => {
|
||||||
render(
|
render(<DeploymentStatusMessage deployment={defaultDeployment} />);
|
||||||
<DeploymentStatusMessage
|
|
||||||
deployment={defaultDeployment}
|
|
||||||
appCreatedAt="2023-02-24"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
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',
|
deploymentStatus: 'DEPLOYED',
|
||||||
deploymentEndedAt: null,
|
deploymentEndedAt: null,
|
||||||
}}
|
}}
|
||||||
appCreatedAt="2023-02-24"
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,19 +62,8 @@ test('should render "deployed 1 day ago" when the deployment has ended', () => {
|
|||||||
deploymentStatus: 'DEPLOYED',
|
deploymentStatus: 'DEPLOYED',
|
||||||
deploymentEndedAt: '2023-02-24T12:15:00.000Z',
|
deploymentEndedAt: '2023-02-24T12:15:00.000Z',
|
||||||
}}
|
}}
|
||||||
appCreatedAt="2023-02-24"
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText(/deployed 1 day ago/i)).toBeInTheDocument();
|
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 { Text } from '@/components/ui/v2/Text';
|
||||||
import type { Deployment } from '@/types/application';
|
import type { Deployment } from '@/types/application';
|
||||||
import formatDistance from 'date-fns/formatDistance';
|
import formatDistance from 'date-fns/formatDistance';
|
||||||
|
|
||||||
export interface DeploymentStatusMessageProps {
|
export interface DeploymentStatusMessageProps {
|
||||||
/**
|
|
||||||
* The deployment to render the status message for.
|
|
||||||
*/
|
|
||||||
deployment: Partial<Deployment>;
|
deployment: Partial<Deployment>;
|
||||||
/**
|
|
||||||
* The date the application was created.
|
|
||||||
*/
|
|
||||||
appCreatedAt: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DeploymentStatusMessage({
|
export default function DeploymentStatusMessage({
|
||||||
deployment,
|
deployment,
|
||||||
appCreatedAt,
|
|
||||||
}: DeploymentStatusMessageProps) {
|
}: DeploymentStatusMessageProps) {
|
||||||
const isDeployingToProduction = [
|
const isDeployingToProduction = [
|
||||||
'SCHEDULED',
|
'SCHEDULED',
|
||||||
@@ -29,11 +21,10 @@ export default function DeploymentStatusMessage({
|
|||||||
(deployment && !deployment.deploymentEndedAt)
|
(deployment && !deployment.deploymentEndedAt)
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<span className="flex flex-row">
|
<span className="flex flex-row justify-start">
|
||||||
<Avatar
|
<Avatar
|
||||||
component="span"
|
alt={`Avatar of ${deployment.commitUserName}`}
|
||||||
name={deployment.commitUserName}
|
src={deployment.commitUserAvatarUrl}
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
|
||||||
className="mr-1 h-4 w-4 self-center"
|
className="mr-1 h-4 w-4 self-center"
|
||||||
/>
|
/>
|
||||||
<Text component="span" className="self-center text-sm">
|
<Text component="span" className="self-center text-sm">
|
||||||
@@ -44,30 +35,26 @@ export default function DeploymentStatusMessage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isDeployingToProduction && deployment?.deploymentEndedAt) {
|
if (!isDeployingToProduction && deployment?.deploymentEndedAt) {
|
||||||
|
const statusMessage = `deployed ${formatDistance(new Date(deployment.deploymentEndedAt), new Date(), { addSuffix: true })}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="grid grid-flow-col">
|
<div className="relative flex flex-row">
|
||||||
<Avatar
|
<Avatar
|
||||||
component="span"
|
alt={`Avatar of ${deployment.commitUserName}`}
|
||||||
name={deployment.commitUserName}
|
src={deployment.commitUserAvatarUrl}
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
className="mr-2 mt-1 h-4 w-4"
|
||||||
className="mr-1 h-4 w-4 self-center"
|
|
||||||
/>
|
/>
|
||||||
<Text component="span" className="self-center truncate text-sm">
|
<div className="flex flex-col text-sm text-muted-foreground">
|
||||||
{deployment.commitUserName} deployed{' '}
|
<p className="line-clamp-1 break-all">{deployment.commitUserName}</p>
|
||||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
<p>{statusMessage}</p>
|
||||||
addSuffix: true,
|
</div>
|
||||||
})}
|
</div>
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text component="span" className="text-sm">
|
<Text component="span" className="text-sm text-muted-foreground">
|
||||||
created{' '}
|
No deployments
|
||||||
{formatDistance(new Date(appCreatedAt), new Date(), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function useGetAppUsers({
|
|||||||
offset = 0,
|
offset = 0,
|
||||||
options = {},
|
options = {},
|
||||||
}: UseFilesOptions) {
|
}: UseFilesOptions) {
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||||
const { data, error, loading } = useRemoteAppGetUsersCustomQuery({
|
const { data, error, loading } = useRemoteAppGetUsersCustomQuery({
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function useAppClient(
|
|||||||
options?: UseAppClientOptions,
|
options?: UseAppClientOptions,
|
||||||
): UseAppClientReturn {
|
): UseAppClientReturn {
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
|
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
return new NhostClient({
|
return new NhostClient({
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
|||||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||||
import {
|
import {
|
||||||
GetProjectDocument,
|
GetProjectDocument,
|
||||||
useGetProjectQuery,
|
|
||||||
type GetProjectQuery,
|
type GetProjectQuery,
|
||||||
type ProjectFragment,
|
type ProjectFragment,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
|
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
type Project = GetProjectQuery['apps'][0];
|
type Project = GetProjectQuery['apps'][0];
|
||||||
|
|
||||||
interface UseProjectOptions {
|
|
||||||
poll?: boolean;
|
|
||||||
target?: 'console-next' | 'user-project';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UseProjectReturnType {
|
export interface UseProjectReturnType {
|
||||||
project: Project;
|
project: Project;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -24,10 +19,7 @@ export interface UseProjectReturnType {
|
|||||||
refetch: (variables?: any) => Promise<any>;
|
refetch: (variables?: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function useProject({
|
export default function useProject(): UseProjectReturnType {
|
||||||
poll = false,
|
|
||||||
target = 'console-next',
|
|
||||||
}: UseProjectOptions = {}): UseProjectReturnType {
|
|
||||||
const {
|
const {
|
||||||
query: { appSubdomain },
|
query: { appSubdomain },
|
||||||
isReady: isRouterReady,
|
isReady: isRouterReady,
|
||||||
@@ -37,65 +29,36 @@ export default function useProject({
|
|||||||
const { isAuthenticated, isLoading: isAuthLoading } =
|
const { isAuthenticated, isLoading: isAuthLoading } =
|
||||||
useAuthenticationStatus();
|
useAuthenticationStatus();
|
||||||
|
|
||||||
const shouldFetchProject =
|
const shouldFetchProject = useMemo(
|
||||||
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],
|
|
||||||
() =>
|
() =>
|
||||||
client.graphql.request<{ apps: ProjectFragment[] }>(GetProjectDocument, {
|
isPlatform &&
|
||||||
|
isAuthenticated &&
|
||||||
|
!isAuthLoading &&
|
||||||
|
!!appSubdomain &&
|
||||||
|
isRouterReady,
|
||||||
|
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, isLoading, refetch, error } = useQuery(
|
||||||
|
['project', appSubdomain as string],
|
||||||
|
async () => {
|
||||||
|
const response = await client.graphql.request<{
|
||||||
|
apps: ProjectFragment[];
|
||||||
|
}>(GetProjectDocument, {
|
||||||
subdomain: (appSubdomain as string) || '',
|
subdomain: (appSubdomain as string) || '',
|
||||||
}),
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
enabled: shouldFetchProject,
|
||||||
enabled: shouldFetchProject && target === 'user-project',
|
|
||||||
staleTime: poll ? 5000 : Infinity, // Adjust staleTime for better performance
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
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) {
|
if (isPlatform) {
|
||||||
return {
|
return {
|
||||||
project,
|
project: data?.data?.apps?.[0] || null,
|
||||||
loading,
|
loading: isLoading && shouldFetchProject,
|
||||||
error,
|
error: Array.isArray(error || {}) ? error[0] : error,
|
||||||
refetch,
|
refetch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as useProjectWithState } from './useProjectWithState';
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
||||||
|
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||||
|
import {
|
||||||
|
GetProjectStateDocument,
|
||||||
|
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];
|
||||||
|
|
||||||
|
export interface UseProjectWithStateReturnType {
|
||||||
|
project: Project;
|
||||||
|
loading?: boolean;
|
||||||
|
error?: Error;
|
||||||
|
refetch: (variables?: any) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useProjectWithState(): UseProjectWithStateReturnType {
|
||||||
|
const {
|
||||||
|
query: { appSubdomain },
|
||||||
|
isReady: isRouterReady,
|
||||||
|
} = useRouter();
|
||||||
|
const client = useNhostClient();
|
||||||
|
const isPlatform = useIsPlatform();
|
||||||
|
const { isAuthenticated, isLoading: isAuthLoading } =
|
||||||
|
useAuthenticationStatus();
|
||||||
|
|
||||||
|
const shouldFetchProject = useMemo(
|
||||||
|
() =>
|
||||||
|
isPlatform &&
|
||||||
|
isAuthenticated &&
|
||||||
|
!isAuthLoading &&
|
||||||
|
!!appSubdomain &&
|
||||||
|
isRouterReady,
|
||||||
|
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, isLoading, refetch, error } = useQuery(
|
||||||
|
['projectWithState', appSubdomain as string],
|
||||||
|
async () => {
|
||||||
|
const response = await client.graphql.request<{
|
||||||
|
apps: ProjectFragment[];
|
||||||
|
}>(GetProjectStateDocument, {
|
||||||
|
subdomain: (appSubdomain as string) || '',
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: shouldFetchProject,
|
||||||
|
keepPreviousData: true,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
refetchInterval: 10000, // poll every 10s
|
||||||
|
staleTime: 1000 * 60 * 5, // 1 minutes
|
||||||
|
cacheTime: 1000 * 60 * 6, //
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPlatform) {
|
||||||
|
return {
|
||||||
|
project: data?.data?.apps?.[0] || null,
|
||||||
|
loading: isLoading && shouldFetchProject,
|
||||||
|
error: Array.isArray(error || {}) ? error[0] : error,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
project: localApplication,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
refetch: () => Promise.resolve(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,54 +1,132 @@
|
|||||||
import { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
|
import { useRemoteApplicationGQLClient } from '@/features/orgs/hooks/useRemoteApplicationGQLClient';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import type { MetricsCardProps } from '@/features/orgs/projects/overview/components/MetricsCard';
|
import type { MetricsCardProps } from '@/features/orgs/projects/overview/components/MetricsCard';
|
||||||
import { MetricsCard } from '@/features/orgs/projects/overview/components/MetricsCard';
|
import { MetricsCard } from '@/features/orgs/projects/overview/components/MetricsCard';
|
||||||
import { prettifyNumber } from '@/utils/prettifyNumber';
|
import { prettifyNumber } from '@/utils/prettifyNumber';
|
||||||
import { prettifySize } from '@/utils/prettifySize';
|
import {
|
||||||
import { useGetProjectMetricsQuery } from '@/utils/__generated__/graphql';
|
useGetProjectMetricsQuery,
|
||||||
|
useGetProjectRequestsMetricQuery,
|
||||||
|
useGetUserProjectMetricsQuery,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { prettifySize } from '@/utils/prettifySize';
|
||||||
|
import { formatISO, startOfDay, startOfMonth, subMinutes } from 'date-fns';
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
export default function OverviewMetrics() {
|
export default function OverviewMetrics() {
|
||||||
const { project } = useProject();
|
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: {
|
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,
|
subdomain: project?.subdomain,
|
||||||
from: new Date(now.getFullYear(), now.getMonth(), 1),
|
from: new Date(now.getFullYear(), now.getMonth(), 1),
|
||||||
},
|
},
|
||||||
skip: !project?.id,
|
skip: !project,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cardElements: MetricsCardProps[] = [
|
const cardElements: MetricsCardProps[] = [
|
||||||
{
|
{
|
||||||
label: 'CPU Usage Seconds',
|
label: 'Daily Active Users',
|
||||||
tooltip: 'Total time the service has used the CPUs',
|
tooltip: 'Unique users active today',
|
||||||
value: prettifyNumber(data?.cpuSecondsUsage?.value || 0),
|
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',
|
label: 'Total Requests',
|
||||||
tooltip:
|
tooltip: 'Total service requests this month so far (excluding functions)',
|
||||||
'Total amount of requests your services have received excluding functions',
|
value: prettifyNumber(totalRequests || 0, {
|
||||||
value: prettifyNumber(data?.totalRequests?.value || 0, {
|
numberOfDecimals: totalRequests > 1000 ? 2 : 0,
|
||||||
numberOfDecimals: data?.totalRequests?.value > 1000 ? 2 : 0,
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Function Invocations',
|
label: 'Egress',
|
||||||
tooltip: 'Number of times your functions have been called',
|
tooltip: 'Total outgoing data transfer this month so far',
|
||||||
value: prettifyNumber(data?.functionInvocations?.value || 0, {
|
value: prettifySize(egressVolume),
|
||||||
numberOfDecimals: 0,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Logs',
|
label: 'Functions Duration',
|
||||||
tooltip: 'Amount of logs stored',
|
tooltip: 'Total Functions execution this month so far',
|
||||||
value: prettifySize(data?.logsVolume?.value || 0),
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
|||||||
value: { fetchBlob, id, mimeType, alt, blob },
|
value: { fetchBlob, id, mimeType, alt, blob },
|
||||||
fallbackPreview = null,
|
fallbackPreview = null,
|
||||||
}: DataGridPreviewCellProps<TData>) {
|
}: DataGridPreviewCellProps<TData>) {
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const appClient = useAppClient();
|
const appClient = useAppClient();
|
||||||
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
|
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
|
||||||
const [showModal, setShowModal] = useState(false);
|
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 { useBuckets } from '@/features/orgs/projects/storage/dataGrid/hooks/useBuckets';
|
||||||
import { useFiles } from '@/features/orgs/projects/storage/dataGrid/hooks/useFiles';
|
import { useFiles } from '@/features/orgs/projects/storage/dataGrid/hooks/useFiles';
|
||||||
import { useFilesAggregate } from '@/features/orgs/projects/storage/dataGrid/hooks/useFilesAggregate';
|
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 type { Files } from '@/utils/__generated__/graphql';
|
||||||
import { Order_By as OrderBy } 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 debounce from 'lodash.debounce';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
@@ -32,7 +32,7 @@ export type FilesDataGridProps = Partial<DataGridProps<StoredFile>>;
|
|||||||
|
|
||||||
export default function FilesDataGrid(props: FilesDataGridProps) {
|
export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const appClient = useAppClient();
|
const appClient = useAppClient();
|
||||||
const [searchString, setSearchString] = useState<string | null>(null);
|
const [searchString, setSearchString] = useState<string | null>(null);
|
||||||
const [currentOffset, setCurrentOffset] = useState<number | null>(
|
const [currentOffset, setCurrentOffset] = useState<number | null>(
|
||||||
@@ -118,7 +118,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
|||||||
DataGridPreviewCell({
|
DataGridPreviewCell({
|
||||||
...cellProps,
|
...cellProps,
|
||||||
fallbackPreview: (
|
fallbackPreview: (
|
||||||
<FilePreviewIcon className="w-5 h-5 fill-current" />
|
<FilePreviewIcon className="h-5 w-5 fill-current" />
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import { useAppClient } from '@/features/orgs/projects/hooks/useAppClient';
|
|||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
|
import type { FileUploadButtonProps } from '@/features/orgs/projects/storage/dataGrid/components/FileUploadButton';
|
||||||
import { FileUploadButton } 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 { getHasuraAdminSecret } from '@/utils/env';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import type { Files } from '@/utils/__generated__/graphql';
|
|
||||||
import type { PropsWithoutRef } from 'react';
|
import type { PropsWithoutRef } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Row } from 'react-table';
|
import type { Row } from 'react-table';
|
||||||
@@ -38,7 +38,7 @@ export default function FilesDataGridControls({
|
|||||||
...props
|
...props
|
||||||
}: FilesDataGridControlsProps) {
|
}: FilesDataGridControlsProps) {
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const appClient = useAppClient();
|
const appClient = useAppClient();
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ export default function FilesDataGridControls({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<Input
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'col-span-12 xs+:col-span-12 md:col-span-9 xl:col-span-10',
|
'col-span-12 xs+:col-span-12 md:col-span-9 xl:col-span-10',
|
||||||
@@ -170,7 +170,7 @@ export default function FilesDataGridControls({
|
|||||||
{...restFilterProps}
|
{...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
|
<DataGridPagination
|
||||||
className={twMerge('col-span-6', paginationClassName)}
|
className={twMerge('col-span-6', paginationClassName)}
|
||||||
{...restPaginationProps}
|
{...restPaginationProps}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import { getHasuraAdminSecret } from '@/utils/env';
|
|
||||||
import type {
|
import type {
|
||||||
Files_Order_By as FilesOrderBy,
|
Files_Order_By as FilesOrderBy,
|
||||||
GetFilesQuery,
|
GetFilesQuery,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useGetFilesQuery } from '@/utils/__generated__/graphql';
|
import { useGetFilesQuery } from '@/utils/__generated__/graphql';
|
||||||
|
import { getHasuraAdminSecret } from '@/utils/env';
|
||||||
import type { QueryHookOptions } from '@apollo/client';
|
import type { QueryHookOptions } from '@apollo/client';
|
||||||
|
|
||||||
export type UseFilesOptions = {
|
export type UseFilesOptions = {
|
||||||
@@ -38,7 +38,7 @@ export default function useFiles({
|
|||||||
orderBy,
|
orderBy,
|
||||||
options = {},
|
options = {},
|
||||||
}: UseFilesOptions) {
|
}: UseFilesOptions) {
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const { data, previousData, ...rest } = useGetFilesQuery({
|
const { data, previousData, ...rest } = useGetFilesQuery({
|
||||||
variables: {
|
variables: {
|
||||||
where: searchString
|
where: searchString
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export default function useNotFoundRedirect() {
|
|||||||
router.pathname === '/account' ||
|
router.pathname === '/account' ||
|
||||||
router.pathname === '/support/ticket' ||
|
router.pathname === '/support/ticket' ||
|
||||||
router.pathname === '/run-one-click-install' ||
|
router.pathname === '/run-one-click-install' ||
|
||||||
|
router.pathname.includes('/orgs/_') ||
|
||||||
|
router.pathname.includes('/orgs/_/projects/_') ||
|
||||||
orgSlug ||
|
orgSlug ||
|
||||||
(orgSlug && appSubdomain) ||
|
(orgSlug && appSubdomain) ||
|
||||||
// If we are on a valid workspace and project, we don't want to redirect to 404
|
// 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 {
|
graphite {
|
||||||
assistants {
|
assistants {
|
||||||
assistantID
|
assistantID
|
||||||
@@ -28,6 +28,7 @@ query getAssistants {
|
|||||||
required
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
dashboard/src/gql/organizations/getProjectState.gql
Normal file
23
dashboard/src/gql/organizations/getProjectState.gql
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
query getProjectState($subdomain: String!) {
|
||||||
|
apps(where: { subdomain: { _eq: $subdomain } }) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
subdomain
|
||||||
|
region {
|
||||||
|
id
|
||||||
|
countryCode
|
||||||
|
name
|
||||||
|
domain
|
||||||
|
city
|
||||||
|
}
|
||||||
|
createdAt
|
||||||
|
desiredState
|
||||||
|
appStates(order_by: { createdAt: desc }, limit: 1) {
|
||||||
|
id
|
||||||
|
appId
|
||||||
|
message
|
||||||
|
stateId
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@ query getProjects($orgSlug: String!) {
|
|||||||
slug
|
slug
|
||||||
createdAt
|
createdAt
|
||||||
subdomain
|
subdomain
|
||||||
|
region {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
deployments(limit: 4, order_by: { deploymentStartedAt: desc }) {
|
deployments(limit: 4, order_by: { deploymentStartedAt: desc }) {
|
||||||
id
|
id
|
||||||
commitSHA
|
commitSHA
|
||||||
@@ -20,5 +24,12 @@ query getProjects($orgSlug: String!) {
|
|||||||
email
|
email
|
||||||
displayName
|
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||||
import {
|
import {
|
||||||
useGetAssistantsQuery,
|
useGetAssistantsQuery,
|
||||||
type GetAssistantsQuery,
|
type GetAssistantsQuery
|
||||||
} from '@/utils/__generated__/graphite.graphql';
|
} from '@/utils/__generated__/graphite.graphql';
|
||||||
import { useMemo, type ReactElement } from 'react';
|
import { useMemo, type ReactElement } from 'react';
|
||||||
|
|
||||||
@@ -29,21 +29,29 @@ export type Assistant = Omit<
|
|||||||
export default function AssistantsPage() {
|
export default function AssistantsPage() {
|
||||||
const { openDrawer } = useDialog();
|
const { openDrawer } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
|
||||||
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
||||||
const { adminClient } = useAdminApolloClient();
|
const { adminClient } = useAdminApolloClient();
|
||||||
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
const { isGraphiteEnabled } = useIsGraphiteEnabled();
|
||||||
|
|
||||||
const { data, loading, refetch } = useGetAssistantsQuery({
|
const {
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
} = useGetAssistantsQuery({
|
||||||
client: adminClient,
|
client: adminClient,
|
||||||
});
|
});
|
||||||
|
|
||||||
const assistants = useMemo(() => data?.graphite?.assistants || [], [data]);
|
const assistants = useMemo(
|
||||||
|
() => data?.graphite?.assistants || [],
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
const openCreateAssistantForm = () => {
|
const openCreateAssistantForm = () => {
|
||||||
openDrawer({
|
openDrawer({
|
||||||
title: 'Create a new Assistant',
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="w-full p-6"
|
className="w-full p-6"
|
||||||
@@ -141,13 +153,11 @@ export default function AssistantsPage() {
|
|||||||
New
|
New
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<div>
|
<AssistantsList
|
||||||
<AssistantsList
|
assistants={assistants}
|
||||||
assistants={assistants}
|
onDelete={() => refetch()}
|
||||||
onDelete={() => refetch()}
|
onCreateOrUpdate={() => refetch()}
|
||||||
onCreateOrUpdate={() => refetch()}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Text } from '@/components/ui/v2/Text';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useGetAssistantsQuery,
|
useGetAssistantsQuery,
|
||||||
|
useGetGraphiteFileStoresQuery,
|
||||||
type GetAssistantsQuery,
|
type GetAssistantsQuery,
|
||||||
} from '@/utils/__generated__/graphite.graphql';
|
} from '@/utils/__generated__/graphite.graphql';
|
||||||
import { useMemo, type ReactElement } from 'react';
|
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 { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||||
import { AssistantsList } from '@/features/orgs/projects/ai/AssistantsList';
|
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 { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||||
@@ -43,24 +45,46 @@ export default function AssistantsPage() {
|
|||||||
const { isGraphiteEnabled, loading: loadingGraphite } =
|
const { isGraphiteEnabled, loading: loadingGraphite } =
|
||||||
useIsGraphiteEnabled();
|
useIsGraphiteEnabled();
|
||||||
|
|
||||||
const {
|
const { isFileStoreSupported, loading: fileStoreLoading } =
|
||||||
data,
|
useIsFileStoreSupported();
|
||||||
loading: loadingAssistants,
|
|
||||||
refetch,
|
|
||||||
} = useGetAssistantsQuery({
|
|
||||||
client: adminClient,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 = () => {
|
const openCreateAssistantForm = () => {
|
||||||
openDrawer({
|
openDrawer({
|
||||||
title: 'Create a new Assistant',
|
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 (
|
return (
|
||||||
<Box className="flex items-center justify-center w-full h-full">
|
<Box className="flex items-center justify-center w-full h-full">
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -114,7 +138,7 @@ export default function AssistantsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.graphite?.assistants.length === 0 && !loadingAssistants) {
|
if (assistants.length === 0 && !assistantsLoading) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="w-full p-6"
|
className="w-full p-6"
|
||||||
@@ -161,8 +185,9 @@ export default function AssistantsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<AssistantsList
|
<AssistantsList
|
||||||
assistants={assistants}
|
assistants={assistants}
|
||||||
onDelete={() => refetch()}
|
fileStores={isFileStoreSupported ? fileStores : undefined}
|
||||||
onCreateOrUpdate={() => refetch()}
|
onDelete={() => assistantsRefetch()}
|
||||||
|
onCreateOrUpdate={() => assistantsRefetch()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -12,14 +12,13 @@ import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
|||||||
import { Link } from '@/components/ui/v2/Link';
|
import { Link } from '@/components/ui/v2/Link';
|
||||||
import { Text } from '@/components/ui/v2/Text';
|
import { Text } from '@/components/ui/v2/Text';
|
||||||
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
import { AISidebar } from '@/features/orgs/layout/AISidebar';
|
||||||
// import AILayout from '@/features/orgs/layout/AILayout/AILayout';
|
|
||||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||||
import { AutoEmbeddingsForm } from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
import { AutoEmbeddingsForm } from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
||||||
import { AutoEmbeddingsList } from '@/features/orgs/projects/ai/AutoEmbeddingsList';
|
import { AutoEmbeddingsList } from '@/features/orgs/projects/ai/AutoEmbeddingsList';
|
||||||
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
import { useIsGraphiteEnabled } from '@/features/orgs/projects/common/hooks/useIsGraphiteEnabled';
|
||||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
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 { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||||
import {
|
import {
|
||||||
useGetGraphiteAutoEmbeddingsConfigurationsQuery,
|
useGetGraphiteAutoEmbeddingsConfigurationsQuery,
|
||||||
@@ -36,9 +35,11 @@ export type AutoEmbeddingsConfiguration = Omit<
|
|||||||
export default function AutoEmbeddingsPage() {
|
export default function AutoEmbeddingsPage() {
|
||||||
const limit = useRef(25);
|
const limit = useRef(25);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { openDrawer } = useDialog();
|
const { openDrawer } = useDialog();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { currentOrg: org } = useOrgs();
|
|
||||||
|
const { org } = useCurrentOrg();
|
||||||
const { project } = useProject();
|
const { project } = useProject();
|
||||||
|
|
||||||
const { adminClient } = useAdminApolloClient();
|
const { adminClient } = useAdminApolloClient();
|
||||||
@@ -101,7 +102,7 @@ export default function AutoEmbeddingsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isPlatform && !org?.plan?.isFree && !project.config?.ai) ||
|
(isPlatform && !org?.plan?.isFree && !project?.config?.ai) ||
|
||||||
!isGraphiteEnabled
|
!isGraphiteEnabled
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@@ -128,7 +129,7 @@ export default function AutoEmbeddingsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.graphiteAutoEmbeddingsConfigurations.length === 0 && !loading) {
|
if (autoEmbeddingsConfigurations.length === 0 && !loading) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="w-full p-6"
|
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 (
|
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-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]">
|
<div className="grid grid-cols-2 gap-2 md:grid-flow-col md:grid-cols-[initial]">
|
||||||
<UserSelect
|
<UserSelect
|
||||||
@@ -250,7 +250,7 @@ function GraphiQLEditor({ onHeaderChange }: GraphiQLEditorProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function GraphQLPage() {
|
export default function GraphQLPage() {
|
||||||
const { project } = useProject({ target: 'user-project' });
|
const { project } = useProject();
|
||||||
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
|
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
|
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
|
<ControlledAutocomplete
|
||||||
id="services"
|
id="services"
|
||||||
name="services"
|
name="services"
|
||||||
label="services"
|
label="Services"
|
||||||
fullWidth
|
fullWidth
|
||||||
multiple
|
multiple
|
||||||
aria-label="Enabled APIs"
|
aria-label="Services"
|
||||||
options={[
|
options={[
|
||||||
'Dashboard',
|
'Dashboard',
|
||||||
'Database',
|
'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
689
dashboard/src/utils/__generated__/graphql.ts
generated
689
dashboard/src/utils/__generated__/graphql.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
|||||||
# @nhost/docs
|
# @nhost/docs
|
||||||
|
|
||||||
|
## 2.24.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- a99f034: chore: fix function name
|
||||||
|
|
||||||
## 2.23.0
|
## 2.23.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ nhost
|
|||||||
│ │ ├── password-reset
|
│ │ ├── password-reset
|
||||||
│ │ │ ├── body.html
|
│ │ │ ├── body.html
|
||||||
│ │ │ └── subject.txt
|
│ │ │ └── subject.txt
|
||||||
|
│ │ ├── signin-otp
|
||||||
|
│ │ │ ├── body.html
|
||||||
|
│ │ │ └── subject.txt
|
||||||
│ │ ├── signin-passwordless
|
│ │ ├── signin-passwordless
|
||||||
│ │ │ ├── body.html
|
│ │ │ ├── body.html
|
||||||
│ │ │ └── subject.txt
|
│ │ │ └── subject.txt
|
||||||
@@ -49,6 +52,9 @@ nhost
|
|||||||
│ ├── password-reset
|
│ ├── password-reset
|
||||||
│ │ ├── body.html
|
│ │ ├── body.html
|
||||||
│ │ └── subject.txt
|
│ │ └── subject.txt
|
||||||
|
│ ├── signin-otp
|
||||||
|
│ │ ├── body.html
|
||||||
|
│ │ └── subject.txt
|
||||||
│ ├── signin-passwordless
|
│ ├── signin-passwordless
|
||||||
│ │ ├── body.html
|
│ │ ├── body.html
|
||||||
│ │ └── subject.txt
|
│ │ └── subject.txt
|
||||||
@@ -82,7 +88,7 @@ The following variables are available to all email templates:
|
|||||||
| `serverUrl` | URL of the authentication server |
|
| `serverUrl` | URL of the authentication server |
|
||||||
| `clientUrl` | URL of your client app |
|
| `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 |
|
| `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 |
|
| `displayName` | The display name of the user |
|
||||||
| `email` | The email of the user |
|
| `email` | The email of the user |
|
||||||
| `locale` | Locale of the user as a two-letter language code (e.g. "en") |
|
| `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
|
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
|
> 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.
|
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` :
|
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",
|
"name": "@nhost/docs",
|
||||||
"version": "2.23.0",
|
"version": "2.24.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "mintlify dev"
|
"start": "mintlify dev"
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ Note: The Nhost client automatically refreshes the session when the user is auth
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
// Refresh the session with the the current internal refresh token.
|
// Refresh the session with the the current internal refresh token.
|
||||||
nhost.auth.refreshToken()
|
nhost.auth.refreshSession()
|
||||||
|
|
||||||
// Refresh the session with an external refresh token.
|
// Refresh the session with an external refresh token.
|
||||||
nhost.auth.refreshToken(refreshToken)
|
nhost.auth.refreshSession(refreshToken)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"checkJs": true,
|
"checkJs": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"moduleResolution": "node",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true
|
"strict": true
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nhost/nhost-js": "^3.1.5",
|
|
||||||
"@playwright/test": "^1.41.0",
|
"@playwright/test": "^1.41.0",
|
||||||
"@sveltejs/adapter-auto": "^2.1.1",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
"@sveltejs/kit": "^1.30.4",
|
"@sveltejs/kit": "^2.11.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
@@ -25,15 +25,16 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^2.10.1",
|
||||||
"svelte": "^4.2.19",
|
"svelte": "^5.14.0",
|
||||||
"svelte-check": "^3.6.8",
|
"svelte-check": "^3.6.8",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.3",
|
"typescript": "^5.4.3",
|
||||||
"vite": "^5.4.6",
|
"vite": "^6.0.3",
|
||||||
"vitest": "^0.25.8"
|
"vitest": "^0.25.8"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nhost/nhost-js": "^3.2.1",
|
||||||
"graphql": "16.8.1",
|
"graphql": "16.8.1",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"js-cookie": "^3.0.5",
|
"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({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['@nhost/nhos-js']
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [/@nhost\/nhos-js/, /node_modules/]
|
||||||
|
}
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000
|
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.getByPlaceholder(/new email/i).fill(newEmail)
|
||||||
|
|
||||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
|
||||||
await newPage
|
await newPage
|
||||||
.locator('div')
|
.locator('div')
|
||||||
.filter({ hasText: /^Change emailChange$/ })
|
.filter({ hasText: /^Change emailChange$/ })
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
.click()
|
.click()
|
||||||
|
|
||||||
// await expect(
|
await expect(
|
||||||
// newPage.getByText(/please check your inbox and follow the link to confirm the email change./i)
|
newPage.getByText('Please check your inbox and follow the link to confirm the email change.')
|
||||||
// ).toBeVisible()
|
).toBeVisible()
|
||||||
|
|
||||||
await expect(newPage.getByRole('status')).toContainText(
|
|
||||||
'Please check your inbox and follow the link to confirm the email change.'
|
|
||||||
)
|
|
||||||
|
|
||||||
await newPage.getByRole('link', { name: /sign out/i }).click()
|
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'
|
requestType: 'email-confirm-change'
|
||||||
})
|
})
|
||||||
|
|
||||||
// await expect(updatedEmailPage.getByText(/profile page/i)).toBeVisible()
|
|
||||||
await expect(updatedEmailPage.getByRole('heading', { name: /profile/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()
|
const newEmail = faker.random.alphaNumeric()
|
||||||
|
|
||||||
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
await newPage.getByPlaceholder(/new email/i).fill(newEmail)
|
||||||
// await newPage.locator('h1:has-text("Change email") + div button:has-text("Change")').click()
|
|
||||||
|
|
||||||
await newPage
|
await newPage
|
||||||
.locator('div')
|
.locator('div')
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@vue/test-utils": "^2.4.5",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"pnpm": "^7.33.7",
|
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"unocss": "^0.33.5",
|
"unocss": "^0.33.5",
|
||||||
"unplugin-auto-import": "^0.17.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:dashboard": "turbo run build --filter=@nhost/dashboard",
|
||||||
"build:docs": "turbo run build --filter=@nhost/docs",
|
"build:docs": "turbo run build --filter=@nhost/docs",
|
||||||
"build:all": "turbo run build --include-dependencies",
|
"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",
|
"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: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}",
|
"clean": "rm -rf ./{{packages,examples/**}/*,docs,dashboard}/{dist,umd,.next,.turbo,coverage}",
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
"eslint-plugin-vue": "^9.26.0",
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.3.3",
|
||||||
"turbo": "1.11.3",
|
"turbo": "1.11.3",
|
||||||
"typedoc": "^0.22.18",
|
"typedoc": "^0.22.18",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
@@ -91,10 +92,9 @@
|
|||||||
"resolutions": {
|
"resolutions": {
|
||||||
"graphql": "16.8.1"
|
"graphql": "16.8.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@8.10.5",
|
"packageManager": "pnpm@9.15.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18 <19",
|
"node": ">=20"
|
||||||
"pnpm": ">=8.0.0"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "./config/.eslintrc.js"
|
"extends": "./config/.eslintrc.js"
|
||||||
|
|||||||
@@ -804,10 +804,10 @@ export class HasuraAuthClient {
|
|||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* // Refresh the session with the the current internal refresh token.
|
* // Refresh the session with the the current internal refresh token.
|
||||||
* nhost.auth.refreshToken();
|
* nhost.auth.refreshSession();
|
||||||
*
|
*
|
||||||
* // Refresh the session with an external refresh token.
|
* // 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
|
* @docs https://docs.nhost.io/reference/javascript/auth/refresh-session
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../config/tsconfig.base.json",
|
"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/**"],
|
"outputs": ["dist/**"],
|
||||||
"env": ["VITE_NHOST_SUBDOMAIN", "VITE_NHOST_REGION"]
|
"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": {
|
"@nhost-examples/vue-apollo#build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": ["dist/**"],
|
"outputs": ["dist/**"],
|
||||||
|
|||||||
Reference in New Issue
Block a user