fix (dashboard): disable settings when there is no config server env variable present (#3394)
### **PR Type** Bug fix, Enhancement ___ ### **Description** - Disable settings when no config server variable - Improve platform-specific page handling - Enhance error handling and loading states - Refactor environment variable checks ___ ### Diagram Walkthrough ```mermaid flowchart LR A["Environment Check"] --> B["Settings Visibility"] A --> C["Platform-specific Pages"] D["Error Handling"] --> E["Loading States"] F["Code Refactoring"] ``` <details> <summary><h3> File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody></tr></tbody></table> </details> ___ --------- Co-authored-by: David Barroso <dbarrosop@dravetech.com>
This commit is contained in:
5
.changeset/sixty-turtles-fetch.md
Normal file
5
.changeset/sixty-turtles-fetch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
fix (dashboard): Disable settings pages when config server env variable is not set
|
||||
@@ -21,6 +21,6 @@ find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
|
||||
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL:-""}~g" {} +
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function AuthenticatedLayout({
|
||||
const isMdOrLarger = useMediaQuery('md');
|
||||
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const isHealthy = useIsHealthy();
|
||||
const { isHealthy, isLoading: isHealthyLoading } = useIsHealthy();
|
||||
const [mainNavContainer, setMainNavContainer] = useState(null);
|
||||
const { mainNavPinned } = useTreeNavState();
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function AuthenticatedLayout({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlatform && !isHealthy) {
|
||||
if (!isPlatform && !isHealthy && !isHealthyLoading) {
|
||||
return (
|
||||
<BaseLayout className="h-full" {...props}>
|
||||
<Header className="flex max-h-[59px] flex-auto" />
|
||||
|
||||
@@ -12,9 +12,9 @@ import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Badge } from '@/components/ui/v3/badge';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getConfigServerUrl, isPlatform as getIsPlatform } from '@/utils/env';
|
||||
import { Box, ChevronDown, ChevronRight, Plus } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useMemo, type ReactElement } from 'react';
|
||||
@@ -160,7 +160,12 @@ const projectSettingsPages = [
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
];
|
||||
|
||||
const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
const createOrganization = (org: Org) => {
|
||||
const isNotPlatform = !getIsPlatform();
|
||||
const configServerVariableNotSet = getConfigServerUrl() === '';
|
||||
const shouldDisableSettings = isNotPlatform && configServerVariableNotSet;
|
||||
const shouldDisableGraphite = shouldDisableSettings;
|
||||
|
||||
const result = {};
|
||||
|
||||
result[org.slug] = {
|
||||
@@ -211,7 +216,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
slug: 'new',
|
||||
icon: <Plus className="mr-1 h-4 w-4 font-bold" strokeWidth={3} />,
|
||||
targetUrl: `/orgs/${org.slug}/projects/new`,
|
||||
disabled: !isPlatform,
|
||||
disabled: isNotPlatform,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -238,9 +243,9 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
result[`${org.slug}-${_app.subdomain}-${_page.slug}`] = {
|
||||
index: `${org.slug}-${_app.subdomain}-${_page.slug}`,
|
||||
canMove: false,
|
||||
isFolder: _page.name === 'Settings',
|
||||
isFolder: _page.name === 'Settings' && !shouldDisableSettings,
|
||||
children:
|
||||
_page.name === 'Settings'
|
||||
_page.name === 'Settings' && !shouldDisableSettings
|
||||
? projectSettingsPages.map(
|
||||
(p) => `${org.slug}-${_app.subdomain}-settings-${p.slug}`,
|
||||
)
|
||||
@@ -251,9 +256,12 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
isProjectPage: true,
|
||||
targetUrl: `/orgs/${org.slug}/projects/${_app.subdomain}/${_page.route}`,
|
||||
disabled:
|
||||
['deployments', 'backups', 'logs', 'metrics'].includes(
|
||||
(['deployments', 'backups', 'logs', 'metrics'].includes(
|
||||
_page.slug,
|
||||
) && !isPlatform,
|
||||
) &&
|
||||
isNotPlatform) ||
|
||||
(_page.name === 'Settings' && shouldDisableSettings) ||
|
||||
(_page.name === 'AI' && shouldDisableGraphite),
|
||||
},
|
||||
canRename: false,
|
||||
};
|
||||
@@ -272,6 +280,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
p.slug === 'general'
|
||||
? `/orgs/${org.slug}/projects/${_app.subdomain}/settings`
|
||||
: `/orgs/${org.slug}/projects/${_app.subdomain}/settings/${p.route}`,
|
||||
disabled: shouldDisableSettings,
|
||||
},
|
||||
canRename: false,
|
||||
};
|
||||
@@ -286,7 +295,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
data: {
|
||||
name: 'Settings',
|
||||
targetUrl: `/orgs/${org.slug}/settings`,
|
||||
disabled: !isPlatform,
|
||||
disabled: isNotPlatform,
|
||||
},
|
||||
canRename: false,
|
||||
};
|
||||
@@ -299,7 +308,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
data: {
|
||||
name: 'Members',
|
||||
targetUrl: `/orgs/${org.slug}/members`,
|
||||
disabled: !isPlatform,
|
||||
disabled: isNotPlatform,
|
||||
},
|
||||
canRename: false,
|
||||
};
|
||||
@@ -312,7 +321,7 @@ const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
data: {
|
||||
name: 'Billing',
|
||||
targetUrl: `/orgs/${org.slug}/billing`,
|
||||
disabled: !isPlatform,
|
||||
disabled: isNotPlatform,
|
||||
},
|
||||
canRename: false,
|
||||
};
|
||||
@@ -333,7 +342,6 @@ type NavItem = {
|
||||
|
||||
const buildNavTreeData = (
|
||||
org: Org,
|
||||
isPlatform: boolean,
|
||||
): { items: Record<TreeItemIndex, TreeItem<NavItem>> } => {
|
||||
if (!org) {
|
||||
return {
|
||||
@@ -365,7 +373,7 @@ const buildNavTreeData = (
|
||||
data: { name: 'root' },
|
||||
canRename: false,
|
||||
},
|
||||
...createOrganization(org, isPlatform),
|
||||
...createOrganization(org),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -374,11 +382,8 @@ const buildNavTreeData = (
|
||||
|
||||
export default function NavTree() {
|
||||
const { currentOrg: org } = useOrgs();
|
||||
const isPlatform = useIsPlatform();
|
||||
const navTree = useMemo(
|
||||
() => buildNavTreeData(org, isPlatform),
|
||||
[org, isPlatform],
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const navTree = useMemo(() => buildNavTreeData(org), [org?.slug]);
|
||||
const { orgsTreeViewState, setOrgsTreeViewState, setOpen } =
|
||||
useTreeNavState();
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ export default function SocialProvidersSettings() {
|
||||
);
|
||||
|
||||
const github = useMemo(
|
||||
() =>
|
||||
nhost.auth.signInProviderURL('github', {
|
||||
connect: token,
|
||||
redirectTo: `${window.location.origin}/account`,
|
||||
}),
|
||||
() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return nhost.auth.signInProviderURL('github', {
|
||||
connect: token,
|
||||
redirectTo: `${window.location.origin}/account`,
|
||||
});
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[token],
|
||||
);
|
||||
|
||||
@@ -36,7 +36,8 @@ export default function TransferOrUpgradeProjectDialog({
|
||||
}, [session_id, setOpen, isRouterReady]);
|
||||
|
||||
const path = asPath.split('?')[0];
|
||||
const redirectUrl = `${window.location.origin}${path}`;
|
||||
const redirectUrl =
|
||||
typeof window !== 'undefined' ? `${window.location.origin}${path}` : '';
|
||||
|
||||
const handleCreateDialogOpenStateChange = (newState: boolean) => {
|
||||
setShowCreateOrgModal(newState);
|
||||
|
||||
@@ -16,11 +16,36 @@ import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getConfigServerUrl, isPlatform as isPlatformFn } from '@/utils/env';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useMemo, type ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, type ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const platFormOnlyPages = [
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments',
|
||||
'/orgs[orgSlug]/projects/[appSubdomain]/backups',
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/logs',
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/metrics',
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments/[deploymentId]',
|
||||
];
|
||||
|
||||
function isSelfHostedAndGraphitePage(route: string) {
|
||||
const isGraphitePage = route.startsWith(
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/ai',
|
||||
);
|
||||
const isConfigEnvVariableNotSet = getConfigServerUrl() === '';
|
||||
|
||||
return isGraphitePage && isConfigEnvVariableNotSet;
|
||||
}
|
||||
|
||||
function isPlatformOnlyPage(route: string) {
|
||||
const platFormOnlyPage = !!platFormOnlyPages.find((page) => route === page);
|
||||
const isNotPlatform = !isPlatformFn();
|
||||
|
||||
return isNotPlatform && platFormOnlyPage;
|
||||
}
|
||||
|
||||
export interface ProjectLayoutContentProps extends AuthenticatedLayoutProps {
|
||||
/**
|
||||
* Props passed to the internal `<main />` element.
|
||||
@@ -35,10 +60,12 @@ function ProjectLayoutContent({
|
||||
const {
|
||||
route,
|
||||
query: { appSubdomain },
|
||||
push,
|
||||
} = useRouter();
|
||||
|
||||
const { state } = useAppState();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { project, loading, error } = useProjectWithState();
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
@@ -132,6 +159,16 @@ function ProjectLayoutContent({
|
||||
renderPausedProjectContent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlatformOnlyPage(route) || isSelfHostedAndGraphitePage(route)) {
|
||||
push('/404');
|
||||
}
|
||||
}, [route, push]);
|
||||
|
||||
if (isPlatformOnlyPage(route) || isSelfHostedAndGraphitePage(route)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
|
||||
@@ -3,14 +3,29 @@ import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useSettingsDisabled } from '@/hooks/useSettingsDisabled';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function SettingsLayout({ children }: PropsWithChildren) {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const hasGitRepo = !!project?.githubRepository;
|
||||
const isSettingsDisabled = useSettingsDisabled();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSettingsDisabled) {
|
||||
router.push('/404');
|
||||
}
|
||||
}, [router, isSettingsDisabled]);
|
||||
|
||||
if (isSettingsDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -165,6 +165,8 @@ export default function DevAssistant() {
|
||||
);
|
||||
}
|
||||
|
||||
const slug = isPlatform ? currentOrg?.slug : 'local';
|
||||
|
||||
if (
|
||||
(isPlatform && !currentOrg?.plan?.isFree && !project?.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
@@ -176,7 +178,7 @@ export default function DevAssistant() {
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/orgs/${currentOrg?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function useIsHealthy() {
|
||||
'hasura',
|
||||
);
|
||||
|
||||
const { failureCount, status } = useQuery(
|
||||
const { failureCount, status, isLoading } = useQuery(
|
||||
['/v1/version'],
|
||||
() => fetch(`${appUrl}/v1/version`),
|
||||
{
|
||||
@@ -27,5 +27,8 @@ export default function useIsHealthy() {
|
||||
},
|
||||
);
|
||||
|
||||
return isPlatform || (status === 'success' && failureCount === 0);
|
||||
return {
|
||||
isHealthy: isPlatform || (status === 'success' && failureCount === 0),
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { graphql } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { beforeAll, expect, test } from 'vitest';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
|
||||
|
||||
const server = setupServer(
|
||||
@@ -22,9 +22,11 @@ const server = setupServer(
|
||||
enableConsole: false,
|
||||
devMode: false,
|
||||
enabledAPIs: [],
|
||||
inferFunctionPermissions: false,
|
||||
},
|
||||
logs: [],
|
||||
events: [],
|
||||
resources: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -32,62 +34,70 @@ const server = setupServer(
|
||||
),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
describe('HasuraCorsDomainSettings', () => {
|
||||
vi.stubEnv(
|
||||
'NEXT_PUBLIC_NHOST_CONFIGSERVER_URL',
|
||||
'https://my-config-server.com',
|
||||
);
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('should not enable switch by default when CORS domain is set to *', async () => {
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
test('should not enable switch by default when CORS domain is set to *', async () => {
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
|
||||
server.use(
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
hasura: {
|
||||
version: 'v2.25.1-ce',
|
||||
settings: {
|
||||
corsDomain: ['https://example.com', 'https://*.example.com'],
|
||||
enableAllowList: false,
|
||||
enableRemoteSchemaPermissions: false,
|
||||
enableConsole: false,
|
||||
devMode: false,
|
||||
enabledAPIs: [],
|
||||
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
|
||||
server.use(
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
hasura: {
|
||||
version: 'v2.25.1-ce',
|
||||
settings: {
|
||||
corsDomain: ['https://example.com', 'https://*.example.com'],
|
||||
enableAllowList: false,
|
||||
enableRemoteSchemaPermissions: false,
|
||||
enableConsole: false,
|
||||
devMode: false,
|
||||
enabledAPIs: [],
|
||||
inferFunctionPermissions: false,
|
||||
},
|
||||
logs: [],
|
||||
events: [],
|
||||
resources: [],
|
||||
},
|
||||
logs: [],
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
|
||||
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toHaveValue(
|
||||
'https://example.com, https://*.example.com',
|
||||
);
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toHaveValue(
|
||||
'https://example.com, https://*.example.com',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Org } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { type GetProjectQuery } from '@/utils/__generated__/graphql';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
|
||||
export const localApplication: GetProjectQuery['apps'][0] = {
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
|
||||
1
dashboard/src/hooks/useSettingsDisabled/index.ts
Normal file
1
dashboard/src/hooks/useSettingsDisabled/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useSettingsDisabled } from './useSettingsDisabled';
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getConfigServerUrl, isPlatform } from '@/utils/env';
|
||||
|
||||
export default function useSettingsDisabled() {
|
||||
const noConfigServerEnvVariableSet = getConfigServerUrl() === '';
|
||||
const notPlatform = !isPlatform();
|
||||
|
||||
return notPlatform && noConfigServerEnvVariableSet;
|
||||
}
|
||||
@@ -109,6 +109,7 @@ export default function AssistantsPage() {
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
const slug = isPlatform ? org?.slug : 'local';
|
||||
|
||||
if (
|
||||
(isPlatform && !org?.plan?.isFree && !project.config?.ai) ||
|
||||
@@ -124,7 +125,7 @@ export default function AssistantsPage() {
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
|
||||
@@ -101,6 +101,8 @@ export default function AutoEmbeddingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const slug = isPlatform ? org?.slug : 'local';
|
||||
|
||||
if (
|
||||
(isPlatform && !org?.plan?.isFree && !project?.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
@@ -115,7 +117,7 @@ export default function AutoEmbeddingsPage() {
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
|
||||
@@ -83,6 +83,8 @@ export default function FileStoresPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const slug = isPlatform ? org?.slug : 'local';
|
||||
|
||||
if (
|
||||
(isPlatform && !org?.plan?.isFree && !project.config?.ai) ||
|
||||
!isGraphiteEnabled
|
||||
@@ -97,7 +99,7 @@ export default function FileStoresPage() {
|
||||
<Text component="span">
|
||||
To enable graphite, configure the service first in{' '}
|
||||
<Link
|
||||
href={`/orgs/${org?.slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
href={`/orgs/${slug}/projects/${project?.subdomain}/settings/ai`}
|
||||
rel="noopener noreferrer"
|
||||
underline="hover"
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { GraphiQLInterface } from 'graphiql';
|
||||
import 'graphiql/graphiql.min.css';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import debounce from 'lodash.debounce';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
@@ -209,66 +210,76 @@ function GraphiQLEditor({ onHeaderChange }: GraphiQLEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function GraphQLPage() {
|
||||
const { project } = useProject();
|
||||
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
|
||||
const GraphQLPageContent = dynamic(
|
||||
() =>
|
||||
Promise.resolve(() => {
|
||||
const { project } = useProject();
|
||||
const [userHeaders, setUserHeaders] = useState<Record<string, any>>({});
|
||||
|
||||
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
if (!project?.subdomain || !project?.config?.hasura.adminSecret) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const appUrl = generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
'graphql',
|
||||
);
|
||||
const appUrl = generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const subscriptionUrl = `${appUrl
|
||||
.replace('https', 'wss')
|
||||
.replace('http', 'ws')}`;
|
||||
const subscriptionUrl = `${appUrl
|
||||
.replace('https', 'wss')
|
||||
.replace('http', 'ws')}`;
|
||||
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'x-hasura-admin-secret': project.config?.hasura.adminSecret,
|
||||
...userHeaders,
|
||||
};
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'x-hasura-admin-secret': project.config?.hasura.adminSecret,
|
||||
...userHeaders,
|
||||
};
|
||||
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: appUrl,
|
||||
headers,
|
||||
wsClient: createClient({
|
||||
url: subscriptionUrl,
|
||||
keepAlive: 2000,
|
||||
connectionParams: {
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: appUrl,
|
||||
headers,
|
||||
},
|
||||
wsClient: createClient({
|
||||
url: subscriptionUrl,
|
||||
keepAlive: 2000,
|
||||
connectionParams: {
|
||||
headers,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
function handleUserChange(userId: string) {
|
||||
setUserHeaders((currentHeaders) => ({
|
||||
...currentHeaders,
|
||||
'x-hasura-user-id': userId,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleRoleChange(role: string) {
|
||||
setUserHeaders((currentHeaders) => ({
|
||||
...currentHeaders,
|
||||
'x-hasura-role': role,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphiQLProvider fetcher={fetcher} shouldPersistHeaders>
|
||||
<GraphiQLHeader
|
||||
onUserChange={handleUserChange}
|
||||
onRoleChange={handleRoleChange}
|
||||
/>
|
||||
|
||||
<GraphiQLEditor onHeaderChange={setUserHeaders} />
|
||||
</GraphiQLProvider>
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
function handleUserChange(userId: string) {
|
||||
setUserHeaders((currentHeaders) => ({
|
||||
...currentHeaders,
|
||||
'x-hasura-user-id': userId,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleRoleChange(role: string) {
|
||||
setUserHeaders((currentHeaders) => ({
|
||||
...currentHeaders,
|
||||
'x-hasura-role': role,
|
||||
}));
|
||||
}
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function GraphQLPage() {
|
||||
return (
|
||||
<RetryableErrorBoundary>
|
||||
<GraphiQLProvider fetcher={fetcher} shouldPersistHeaders>
|
||||
<GraphiQLHeader
|
||||
onUserChange={handleUserChange}
|
||||
onRoleChange={handleRoleChange}
|
||||
/>
|
||||
|
||||
<GraphiQLEditor onHeaderChange={setUserHeaders} />
|
||||
</GraphiQLProvider>
|
||||
<GraphQLPageContent />
|
||||
</RetryableErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
5
dashboard/src/utils/env/env.ts
vendored
5
dashboard/src/utils/env/env.ts
vendored
@@ -95,8 +95,5 @@ export function getHasuraApiUrl() {
|
||||
* Custom URL of the config service.
|
||||
*/
|
||||
export function getConfigServerUrl() {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_NHOST_CONFIGSERVER_URL ||
|
||||
'https://local.dashboard.local.nhost.run/v1/configserver/graphql'
|
||||
);
|
||||
return process.env.NEXT_PUBLIC_NHOST_CONFIGSERVER_URL;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.37.1
|
||||
image: nhost/hasura-auth:0.40.2
|
||||
depends_on:
|
||||
graphql:
|
||||
condition: service_healthy
|
||||
@@ -96,7 +96,7 @@ services:
|
||||
read_only: false
|
||||
|
||||
console:
|
||||
image: nhost/graphql-engine:v2.36.9-ce.cli-migrations-v3
|
||||
image: nhost/graphql-engine:v2.46.0-ce.cli-migrations-v3
|
||||
depends_on:
|
||||
graphql:
|
||||
condition: service_healthy
|
||||
@@ -179,7 +179,6 @@ services:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_PUBLIC_NHOST_ADMIN_SECRET: ${GRAPHQL_ADMIN_SECRET}
|
||||
NEXT_PUBLIC_NHOST_AUTH_URL: http://${AUTH_URL}/v1
|
||||
NEXT_PUBLIC_NHOST_CONFIGSERVER_URL: http://unused.in.self.hosting
|
||||
NEXT_PUBLIC_NHOST_FUNCTIONS_URL: http://${FUNCTIONS_URL}/v1
|
||||
NEXT_PUBLIC_NHOST_GRAPHQL_URL: http://${GRAPHQL_URL}/v1/graphql
|
||||
NEXT_PUBLIC_NHOST_HASURA_API_URL: http://${GRAPHQL_URL}
|
||||
@@ -255,7 +254,7 @@ services:
|
||||
read_only: false
|
||||
|
||||
graphql:
|
||||
image: nhost/graphql-engine:v2.36.9-ce
|
||||
image: nhost/graphql-engine:v2.46.0-ce
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -406,7 +405,7 @@ services:
|
||||
read_only: true
|
||||
|
||||
storage:
|
||||
image: nhost/hasura-storage:0.7.1
|
||||
image: nhost/hasura-storage:0.7.2
|
||||
depends_on:
|
||||
graphql:
|
||||
condition: service_healthy
|
||||
|
||||
Reference in New Issue
Block a user