Console 1584 denied access to screen even though i can see the nav link (#7756)

This commit is contained in:
Jonathan Brennan 2026-03-04 08:24:18 -06:00 committed by GitHub
parent ee2785c4cc
commit 548343c64d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 286 additions and 317 deletions

View file

@ -35,13 +35,12 @@ import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { Slot } from '@radix-ui/react-slot';
import { Link, useRouter } from '@tanstack/react-router';
import { useRouter } from '@tanstack/react-router';
import { ProPlanBilling } from '../organization/billing/ProPlanBillingWarm';
import { RateLimitWarn } from '../organization/billing/RateLimitWarn';
import { HiveLink } from '../ui/hive-link';
import { PlusIcon } from '../ui/icon';
import { QueryError } from '../ui/query-error';
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs';
import { OrganizationSelector } from './organization-selectors';
export enum Page {
@ -65,6 +64,9 @@ const OrganizationLayoutQuery = graphql(`
viewerCanManageSupportTickets
viewerCanDescribeBilling
viewerCanSeeMembers
viewerCanAccessSettings
viewerCanManageAccessTokens
viewerCanManagePersonalAccessTokens
...UserMenu_OrganizationFragment
...ProPlanBilling_OrganizationFragment
...RateLimitWarn_OrganizationFragment
@ -80,7 +82,8 @@ export function OrganizationLayout({
children,
page,
className,
...props
organizationSlug,
minimal,
}: {
page?: Page;
className?: string;
@ -92,8 +95,8 @@ export function OrganizationLayout({
const [query] = useQuery({
query: OrganizationLayoutQuery,
variables: {
organizationSlug: props.organizationSlug,
minimal: props.minimal ?? false,
organizationSlug,
minimal: minimal ?? false,
},
requestPolicy: 'cache-first',
});
@ -102,12 +105,12 @@ export function OrganizationLayout({
useLastVisitedOrganizationWriter(currentOrganization?.slug);
if (query.error) {
return <QueryError error={query.error} organizationSlug={props.organizationSlug} />;
return <QueryError error={query.error} organizationSlug={organizationSlug} />;
}
// Only show the null state state if the query has finished fetching and data is not stale
// This prevents showing null state when switching between orgs with cached data
const shouldShowNoOrg = !query.fetching && !query.stale && !currentOrganization && !props.minimal;
const shouldShowNoOrg = !query.fetching && !query.stale && !currentOrganization && !minimal;
return (
<>
@ -115,7 +118,7 @@ export function OrganizationLayout({
<div className="flex flex-row items-center gap-4">
<HiveLink className="size-8" />
<OrganizationSelector
currentOrganizationSlug={props.organizationSlug}
currentOrganizationSlug={organizationSlug}
organizations={query.data?.organizations ?? null}
/>
</div>
@ -125,84 +128,72 @@ export function OrganizationLayout({
organizations={query.data?.organizations ?? null}
/>
</Header>
<SecondaryNavigation>
<div className="container flex items-center justify-between">
{currentOrganization ? (
<Tabs value={page} className="min-w-[600px]">
<TabsList variant="menu">
<TabsTrigger variant="menu" value={Page.Overview} asChild>
<Link
to="/$organizationSlug"
params={{ organizationSlug: currentOrganization.slug }}
>
Overview
</Link>
</TabsTrigger>
{currentOrganization.viewerCanSeeMembers && (
<TabsTrigger variant="menu" value={Page.Members} asChild>
<Link
to="/$organizationSlug/view/members"
params={{ organizationSlug: currentOrganization.slug }}
search={{ page: 'list' }}
>
Members
</Link>
</TabsTrigger>
)}
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationSlug/view/settings"
params={{ organizationSlug: currentOrganization.slug }}
>
Settings
</Link>
</TabsTrigger>
{currentOrganization.viewerCanManageSupportTickets && (
<TabsTrigger variant="menu" value={Page.Support} asChild>
<Link
to="/$organizationSlug/view/support"
params={{ organizationSlug: currentOrganization.slug }}
>
Support
</Link>
</TabsTrigger>
)}
{getIsStripeEnabled() && currentOrganization.viewerCanDescribeBilling && (
<TabsTrigger variant="menu" value={Page.Subscription} asChild>
<Link
to="/$organizationSlug/view/subscription"
params={{ organizationSlug: currentOrganization.slug }}
>
Subscription
</Link>
</TabsTrigger>
)}
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
</div>
)}
{currentOrganization?.viewerCanCreateProject ? (
<SecondaryNavigation
page={page}
loading={!currentOrganization}
className="min-w-[600px]"
links={
currentOrganization
? [
{
value: Page.Overview,
label: 'Overview',
to: '/$organizationSlug',
params: { organizationSlug: currentOrganization.slug },
},
{
value: Page.Members,
label: 'Members',
visible: currentOrganization.viewerCanSeeMembers,
to: '/$organizationSlug/view/members',
params: { organizationSlug: currentOrganization.slug },
search: { page: 'list' },
},
{
value: Page.Settings,
label: 'Settings',
visible:
currentOrganization.viewerCanAccessSettings ||
currentOrganization.viewerCanManageAccessTokens ||
currentOrganization.viewerCanManagePersonalAccessTokens,
to: '/$organizationSlug/view/settings',
params: { organizationSlug: currentOrganization.slug },
},
{
value: Page.Support,
label: 'Support',
visible: currentOrganization.viewerCanManageSupportTickets,
to: '/$organizationSlug/view/support',
params: { organizationSlug: currentOrganization.slug },
},
{
value: Page.Subscription,
label: 'Subscription',
visible: getIsStripeEnabled() && currentOrganization.viewerCanDescribeBilling,
to: '/$organizationSlug/view/subscription',
params: { organizationSlug: currentOrganization.slug },
},
]
: []
}
actions={
currentOrganization?.viewerCanCreateProject ? (
<>
<Button onClick={toggleModalOpen} variant="link" data-cy="new-project-button">
<PlusIcon size={16} className="mr-2" />
New project
</Button>
<CreateProjectModal
organizationSlug={props.organizationSlug}
organizationSlug={organizationSlug}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
// reset the form every time it is closed
key={String(isModalOpen)}
/>
</>
) : null}
</div>
</SecondaryNavigation>
) : null
}
/>
<div className="min-h-(--content-height) container pb-7">
{currentOrganization ? (
<>

View file

@ -21,11 +21,10 @@ import { graphql } from '@/gql';
import { useToggle } from '@/lib/hooks';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { zodResolver } from '@hookform/resolvers/zod';
import { Link, useRouter } from '@tanstack/react-router';
import { useRouter } from '@tanstack/react-router';
import { ResourceNotFoundComponent } from '../resource-not-found';
import { HiveLink } from '../ui/hive-link';
import { PlusIcon } from '../ui/icon';
import { Tabs, TabsList, TabsTrigger } from '../ui/tabs';
import { ProjectSelector } from './project-selector';
export enum Page {
@ -53,6 +52,8 @@ const ProjectLayoutQuery = graphql(`
viewerCanModifySchemaPolicy
viewerCanCreateTarget
viewerCanModifyAlerts
viewerCanModifySettings
viewerCanManageProjectAccessTokens
}
...UserMenu_OrganizationFragment
}
@ -63,7 +64,8 @@ export function ProjectLayout({
children,
page,
className,
...props
organizationSlug,
projectSlug,
}: {
page: Page;
organizationSlug: string;
@ -71,14 +73,13 @@ export function ProjectLayout({
className?: string;
children: ReactNode;
}) {
const params = { organizationSlug, projectSlug };
const [isModalOpen, toggleModalOpen] = useToggle();
const [query] = useQuery({
query: ProjectLayoutQuery,
requestPolicy: 'cache-first',
variables: {
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
},
variables: params,
});
const me = query.data?.me;
@ -93,8 +94,8 @@ export function ProjectLayout({
<div className="flex flex-row items-center gap-4">
<HiveLink className="size-8" />
<ProjectSelector
currentOrganizationSlug={props.organizationSlug}
currentProjectSlug={props.projectSlug}
currentOrganizationSlug={organizationSlug}
currentProjectSlug={projectSlug}
organizations={query.data?.organizations ?? null}
/>
</div>
@ -112,69 +113,54 @@ export function ProjectLayout({
<ResourceNotFoundComponent title="404 - This project does not seem to exist." />
) : (
<>
<SecondaryNavigation>
<div className="container flex items-center justify-between">
{currentOrganization && currentProject ? (
<Tabs value={page}>
<TabsList variant="menu">
<TabsTrigger variant="menu" value={Page.Targets} asChild>
<Link
to="/$organizationSlug/$projectSlug"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
}}
>
Targets
</Link>
</TabsTrigger>
{currentProject.viewerCanModifyAlerts && (
<TabsTrigger variant="menu" value={Page.Alerts} asChild>
<Link
to="/$organizationSlug/$projectSlug/view/alerts"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
}}
>
Alerts
</Link>
</TabsTrigger>
)}
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationSlug/$projectSlug/view/settings"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
}}
>
Settings
</Link>
</TabsTrigger>
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
</div>
)}
{currentProject?.viewerCanCreateTarget ? (
<Button onClick={toggleModalOpen} variant="link">
<PlusIcon size={16} className="mr-2" />
New target
</Button>
) : null}
<CreateTargetModal
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</div>
</SecondaryNavigation>
<SecondaryNavigation
page={page}
loading={!currentOrganization || !currentProject}
links={
currentOrganization && currentProject
? [
{
value: Page.Targets,
label: 'Targets',
to: '/$organizationSlug/$projectSlug',
params,
},
{
value: Page.Alerts,
label: 'Alerts',
visible: currentProject.viewerCanModifyAlerts,
to: '/$organizationSlug/$projectSlug/view/alerts',
params,
},
{
value: Page.Settings,
label: 'Settings',
visible:
currentProject.viewerCanModifySettings ||
currentProject.viewerCanManageProjectAccessTokens,
to: '/$organizationSlug/$projectSlug/view/settings',
params,
},
]
: []
}
actions={
currentProject?.viewerCanCreateTarget ? (
<>
<Button onClick={toggleModalOpen} variant="link">
<PlusIcon size={16} className="mr-2" />
New target
</Button>
<CreateTargetModal
organizationSlug={organizationSlug}
projectSlug={projectSlug}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</>
) : null
}
/>
<div className="min-h-(--content-height) container pb-7">
<div className={className}>{children}</div>
</div>

View file

@ -29,7 +29,6 @@ import { useToggle } from '@/lib/hooks';
import { useResetState } from '@/lib/hooks/use-reset-state';
import { useLastVisitedOrganizationWriter } from '@/lib/last-visited-org';
import { cn } from '@/lib/utils';
import { Link } from '@tanstack/react-router';
import { ResourceNotFoundComponent } from '../resource-not-found';
import { Label } from '../ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
@ -123,7 +122,9 @@ export const TargetLayout = ({
children,
page,
className,
...props
organizationSlug,
projectSlug,
targetSlug,
}: {
page: Page;
organizationSlug: string;
@ -132,15 +133,17 @@ export const TargetLayout = ({
className?: string;
children: ReactNode;
}): ReactElement | null => {
const params = {
organizationSlug,
projectSlug,
targetSlug,
};
const [isModalOpen, toggleModalOpen] = useToggle();
const [query] = useQuery({
query: TargetLayoutQuery,
requestPolicy: 'cache-first',
variables: {
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
},
variables: params,
});
const me = query.data?.me;
@ -155,18 +158,18 @@ export const TargetLayout = ({
return (
<TargetReferenceProvider
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
organizationSlug={organizationSlug}
projectSlug={projectSlug}
targetSlug={targetSlug}
>
<Header>
<div className="flex flex-row items-center gap-4">
<HiveLink className="size-8" />
<TargetSelector
organizations={query.data?.organizations ?? null}
currentOrganizationSlug={props.organizationSlug}
currentProjectSlug={props.projectSlug}
currentTargetSlug={props.targetSlug}
currentOrganizationSlug={organizationSlug}
currentProjectSlug={projectSlug}
currentTargetSlug={targetSlug}
/>
</div>
<div>
@ -184,153 +187,87 @@ export const TargetLayout = ({
<ResourceNotFoundComponent title="404 - This project does not seem to exist." />
) : (
<>
<SecondaryNavigation>
<div className="container flex items-center justify-between">
{currentOrganization && currentProject && currentTarget ? (
<Tabs className="flex h-full grow flex-col" value={page}>
<TabsList variant="menu">
<TabsTrigger variant="menu" value={Page.Schema} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Schema
</Link>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Checks} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/checks"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Checks
</Link>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Explorer} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/explorer"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Explorer
</Link>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.History} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/history/$versionId"
params={{
organizationSlug: currentOrganization.slug,
projectSlug: currentProject.slug,
targetSlug: currentTarget.slug,
versionId: latestSchemaVersion ?? '',
}}
>
History
</Link>
</TabsTrigger>
<TabsTrigger variant="menu" value={Page.Insights} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/insights"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
search={{}}
>
Insights
</Link>
</TabsTrigger>
{currentTarget.viewerCanAccessTraces && (
<TabsTrigger variant="menu" value={Page.Traces} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/traces"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Traces
</Link>
</TabsTrigger>
)}
{currentTarget.viewerCanViewAppDeployments && (
<TabsTrigger variant="menu" value={Page.Apps} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/apps"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Apps
</Link>
</TabsTrigger>
)}
{currentTarget.viewerCanViewLaboratory && (
<TabsTrigger variant="menu" value={Page.Laboratory} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/laboratory"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Laboratory
</Link>
</TabsTrigger>
)}
{currentTarget.viewerCanViewSchemaProposals && (
<TabsTrigger variant="menu" value={Page.Proposals} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/proposals"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Proposals
</Link>
</TabsTrigger>
)}
{currentTarget.viewerCanAccessSettings && (
<TabsTrigger variant="menu" value={Page.Settings} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/settings"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Settings
</Link>
</TabsTrigger>
)}
</TabsList>
</Tabs>
) : (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
</div>
)}
{currentTarget && isCDNEnabled && (
<SecondaryNavigation
page={page}
loading={!currentOrganization || !currentProject || !currentTarget}
className="flex h-full grow flex-col"
links={
currentOrganization && currentProject && currentTarget
? [
{
value: Page.Schema,
label: 'Schema',
to: '/$organizationSlug/$projectSlug/$targetSlug',
params,
},
{
value: Page.Checks,
label: 'Checks',
to: '/$organizationSlug/$projectSlug/$targetSlug/checks',
params,
},
{
value: Page.Explorer,
label: 'Explorer',
to: '/$organizationSlug/$projectSlug/$targetSlug/explorer',
params,
},
{
value: Page.History,
label: 'History',
to: '/$organizationSlug/$projectSlug/$targetSlug/history/$versionId',
params: {
...params,
versionId: latestSchemaVersion ?? '',
},
},
{
value: Page.Insights,
label: 'Insights',
to: '/$organizationSlug/$projectSlug/$targetSlug/insights',
params,
search: {},
},
{
value: Page.Traces,
label: 'Traces',
visible: currentTarget.viewerCanAccessTraces,
to: '/$organizationSlug/$projectSlug/$targetSlug/traces',
params,
},
{
value: Page.Apps,
label: 'Apps',
visible: currentTarget.viewerCanViewAppDeployments,
to: '/$organizationSlug/$projectSlug/$targetSlug/apps',
params,
},
{
value: Page.Laboratory,
label: 'Laboratory',
visible: currentTarget.viewerCanViewLaboratory,
to: '/$organizationSlug/$projectSlug/$targetSlug/laboratory',
params,
},
{
value: Page.Proposals,
label: 'Proposals',
visible: currentTarget.viewerCanViewSchemaProposals,
to: '/$organizationSlug/$projectSlug/$targetSlug/proposals',
params,
},
{
value: Page.Settings,
label: 'Settings',
visible: currentTarget.viewerCanAccessSettings,
to: '/$organizationSlug/$projectSlug/$targetSlug/settings',
params,
},
]
: []
}
actions={
currentTarget && isCDNEnabled ? (
<>
<Button
onClick={toggleModalOpen}
@ -341,16 +278,16 @@ export const TargetLayout = ({
Connect to CDN
</Button>
<ConnectSchemaModal
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
organizationSlug={organizationSlug}
projectSlug={projectSlug}
targetSlug={targetSlug}
isOpen={isModalOpen}
toggleModalOpen={toggleModalOpen}
/>
</>
)}
</div>
</SecondaryNavigation>
) : null
}
/>
<div className={cn('min-h-(--content-height) container pb-7', className)}>{children}</div>
</>
)}

View file

@ -0,0 +1,26 @@
import React, { ComponentProps } from 'react';
import { createLink } from '@tanstack/react-router';
import { TabsTrigger } from '../ui/tabs';
const SecondaryNavLinkComponent = React.forwardRef<
HTMLAnchorElement,
React.AnchorHTMLAttributes<HTMLAnchorElement> & {
value: string;
label: string;
visible?: boolean;
}
>(({ value, label, visible = true, ...anchorProps }, ref) => {
if (!visible) return null;
return (
<TabsTrigger variant="menu" value={value} asChild>
<a ref={ref} {...anchorProps}>
{label}
</a>
</TabsTrigger>
);
});
SecondaryNavLinkComponent.displayName = 'SecondaryNavLinkComponent';
export const SecondaryNavLink = createLink(SecondaryNavLinkComponent);
export type SecondaryNavLinkProps = ComponentProps<typeof SecondaryNavLink>;

View file

@ -1,11 +1,40 @@
type SecondaryNavigationProps = {
children?: React.ReactNode;
};
import { ReactNode } from 'react';
import { Tabs, TabsList } from '../ui/tabs';
import { SecondaryNavLink, type SecondaryNavLinkProps } from './secondary-nav-link';
export function SecondaryNavigation({ children }: SecondaryNavigationProps) {
export function SecondaryNavigation({
page,
loading,
actions,
className,
links,
}: {
page?: string;
loading?: boolean;
actions?: ReactNode;
className?: string;
links: SecondaryNavLinkProps[];
}) {
return (
<div className="h-(--tabs-navbar-height) border-neutral-5 bg-neutral-2 dark:bg-neutral-3 relative border-b">
{children}
<div className="container flex items-center justify-between">
{loading ? (
<div className="flex flex-row gap-x-8 border-b-2 border-b-transparent px-4 py-3">
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
<div className="bg-neutral-5 h-5 w-12 animate-pulse rounded-full" />
</div>
) : (
<Tabs value={page} className={className}>
<TabsList variant="menu">
{links.map(link => (
<SecondaryNavLink key={link.value} {...link} />
))}
</TabsList>
</Tabs>
)}
{actions}
</div>
</div>
);
}