mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Console 1584 denied access to screen even though i can see the nav link (#7756)
This commit is contained in:
parent
ee2785c4cc
commit
548343c64d
5 changed files with 286 additions and 317 deletions
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue