mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 01:28:32 +00:00
Target setting new design (#4241)
Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
This commit is contained in:
parent
23433de2bf
commit
da1d8ac20f
15 changed files with 943 additions and 696 deletions
|
|
@ -8,7 +8,7 @@ if (!document.body.className.includes('dark')) {
|
|||
const preview = {
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
default: 'black',
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ export const TargetLayout = ({
|
|||
projectId: props.projectId,
|
||||
targetId: props.targetId,
|
||||
}}
|
||||
search={{ page: 'general' }}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -32,6 +31,7 @@ import {
|
|||
} from '@/components/ui/dropdown-menu';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { useClipboard } from '@/lib/hooks';
|
||||
|
|
@ -428,21 +428,16 @@ export function OrganizationInvitations(props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<CardTitle>Pending invitations</CardTitle>
|
||||
<CardDescription>
|
||||
Active invitations to join this organization. Invitations expire after 7 days.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div>
|
||||
<MemberInvitationButton
|
||||
refetchInvitations={props.refetchInvitations}
|
||||
organization={organization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Pending invitations"
|
||||
description="Active invitations to join this organization. Invitations expire after 7 days."
|
||||
>
|
||||
<MemberInvitationButton
|
||||
refetchInvitations={props.refetchInvitations}
|
||||
organization={organization}
|
||||
/>
|
||||
</SubPageLayoutHeader>
|
||||
{organization.invitations.nodes.length > 0 ? (
|
||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||
<thead>
|
||||
|
|
@ -476,6 +471,6 @@ export function OrganizationInvitations(props: {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { ChangePermissionsModal } from '@/components/v2/modals';
|
||||
|
|
@ -526,21 +526,16 @@ export function OrganizationMembers(props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<CardTitle>List of organization members</CardTitle>
|
||||
<CardDescription>
|
||||
Manage the members of your organization and their permissions.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div>
|
||||
<MemberInvitationButton
|
||||
refetchInvitations={props.refetchMembers}
|
||||
organization={organization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="List of organization members"
|
||||
description="Manage the members of your organization and their permissions."
|
||||
>
|
||||
<MemberInvitationButton
|
||||
refetchInvitations={props.refetchMembers}
|
||||
organization={organization}
|
||||
/>
|
||||
</SubPageLayoutHeader>
|
||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -589,6 +584,6 @@ export function OrganizationMembers(props: {
|
|||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
||||
import { CardDescription } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -30,6 +30,7 @@ import { ProductUpdatesLink } from '@/components/ui/docs-note';
|
|||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
|
@ -849,22 +850,26 @@ export function OrganizationMemberRolesMigration(props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<CardTitle>Migration Wizard</CardTitle>
|
||||
<CardDescription>
|
||||
This wizard will help you migrate your organization's members to the new permissions
|
||||
system.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
Members are grouped by their access scopes.
|
||||
<br /> You can choose to migrate all members from each group to a new role or assign them
|
||||
to an existing role.
|
||||
</CardDescription>
|
||||
<ProductUpdatesLink href="2023-12-05-member-roles">
|
||||
Read "Introducing Member Roles" product update to learn more.
|
||||
</ProductUpdatesLink>
|
||||
</div>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Migration Wizard"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
This wizard will help you migrate your organization's members to the new permissions
|
||||
system.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
Members are grouped by their access scopes.
|
||||
<br /> You can choose to migrate all members from each group to a new role or assign
|
||||
them to an existing role.
|
||||
</CardDescription>
|
||||
<ProductUpdatesLink href="2023-12-05-member-roles">
|
||||
Read "Introducing Member Roles" product update to learn more.
|
||||
</ProductUpdatesLink>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{organization.unassignedMembersToMigrate.length > 0 ? (
|
||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||
<thead>
|
||||
|
|
@ -900,6 +905,6 @@ export function OrganizationMemberRolesMigration(props: {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -40,6 +39,7 @@ import {
|
|||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
|
@ -894,21 +894,16 @@ export function OrganizationMemberRoles(props: {
|
|||
</AlertDialogContent>
|
||||
) : null}
|
||||
</AlertDialog>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<CardTitle>List of roles</CardTitle>
|
||||
<CardDescription>
|
||||
Manage the roles that can be assigned to members of this organization.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div>
|
||||
<OrganizationMemberRoleCreateButton
|
||||
me={organization.me}
|
||||
organizationCleanId={organization.cleanId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="List of roles"
|
||||
description="Manage the roles that can be assigned to members of this organization."
|
||||
>
|
||||
<OrganizationMemberRoleCreateButton
|
||||
me={organization.me}
|
||||
organizationCleanId={organization.cleanId}
|
||||
/>
|
||||
</SubPageLayoutHeader>
|
||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -930,7 +925,7 @@ export function OrganizationMemberRoles(props: {
|
|||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { useMutation, useQuery } from 'urql';
|
|||
import * as Yup from 'yup';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CardDescription } from '@/components/ui/card';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import {
|
||||
DocsLink,
|
||||
Heading,
|
||||
|
|
@ -354,7 +355,9 @@ export function CDNAccessTokens(props: {
|
|||
|
||||
const closeModal = () => {
|
||||
void router.navigate({
|
||||
search: {},
|
||||
search: {
|
||||
page: 'cdn',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -375,107 +378,112 @@ export function CDNAccessTokens(props: {
|
|||
const canManage = canAccessTarget(TargetAccessScope.Settings, me);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle id="cdn-access-tokens">CDN Access Token</CardTitle>
|
||||
<CardDescription>
|
||||
CDN Access Tokens are used to access to Hive High-Availability CDN and read your schema
|
||||
artifacts.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#cdn-access-tokens"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more about CDN Access Tokens
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{canManage && (
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Button asChild>
|
||||
<Link
|
||||
search={{
|
||||
cdn: 'create',
|
||||
}}
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="CDN Access Token"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
CDN Access Tokens are used to access to Hive High-Availability CDN and read your
|
||||
schema artifacts.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#cdn-access-tokens"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Create new CDN token
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
<TBody>
|
||||
{target?.data?.target?.cdnAccessTokens.edges?.map(edge => {
|
||||
const node = useFragment(CDNAccessTokeRowFragment, edge.node);
|
||||
|
||||
return (
|
||||
<Tr key={node.id}>
|
||||
<Td>
|
||||
{node.firstCharacters + new Array(10).fill('•').join('') + node.lastCharacters}
|
||||
</Td>
|
||||
<Td>{node.alias}</Td>
|
||||
<Td align="right">
|
||||
created <TimeAgo date={node.createdAt} />
|
||||
</Td>
|
||||
<Td align="right">
|
||||
<Button
|
||||
className="hover:text-red-500"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search: {
|
||||
cdn: 'delete',
|
||||
id: edge.node.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
|
||||
<div className="my-3.5 flex justify-end">
|
||||
{target.data?.target?.cdnAccessTokens.pageInfo.hasPreviousPage ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-2 px-5"
|
||||
onClick={() => {
|
||||
setEndCursors(cursors => {
|
||||
if (cursors.length === 0) {
|
||||
return cursors;
|
||||
}
|
||||
return cursors.slice(0, cursors.length - 1);
|
||||
});
|
||||
Learn more about CDN Access Tokens
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{canManage && (
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Button asChild>
|
||||
<Link
|
||||
search={{
|
||||
page: 'cdn',
|
||||
cdn: 'create',
|
||||
}}
|
||||
>
|
||||
Previous Page
|
||||
</Button>
|
||||
) : null}
|
||||
{target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="px-5"
|
||||
onClick={() => {
|
||||
setEndCursors(cursors => {
|
||||
if (!target.data?.target?.cdnAccessTokens.pageInfo.endCursor) {
|
||||
return cursors;
|
||||
}
|
||||
return [...cursors, target.data?.target?.cdnAccessTokens.pageInfo.endCursor];
|
||||
});
|
||||
}}
|
||||
>
|
||||
Next Page
|
||||
</Button>
|
||||
) : null}
|
||||
Create new CDN token
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
<Table>
|
||||
<TBody>
|
||||
{target?.data?.target?.cdnAccessTokens.edges?.map(edge => {
|
||||
const node = useFragment(CDNAccessTokeRowFragment, edge.node);
|
||||
|
||||
return (
|
||||
<Tr key={node.id}>
|
||||
<Td>
|
||||
{node.firstCharacters + new Array(10).fill('•').join('') + node.lastCharacters}
|
||||
</Td>
|
||||
<Td>{node.alias}</Td>
|
||||
<Td align="right">
|
||||
created <TimeAgo date={node.createdAt} />
|
||||
</Td>
|
||||
<Td align="right">
|
||||
<Button
|
||||
className="hover:text-red-500"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search: {
|
||||
page: 'cdn',
|
||||
cdn: 'delete',
|
||||
id: node.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
|
||||
<div className="my-3.5 flex justify-end">
|
||||
{target.data?.target?.cdnAccessTokens.pageInfo.hasPreviousPage ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-2 px-5"
|
||||
onClick={() => {
|
||||
setEndCursors(cursors => {
|
||||
if (cursors.length === 0) {
|
||||
return cursors;
|
||||
}
|
||||
return cursors.slice(0, cursors.length - 1);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Previous Page
|
||||
</Button>
|
||||
) : null}
|
||||
{target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="px-5"
|
||||
onClick={() => {
|
||||
setEndCursors(cursors => {
|
||||
if (!target.data?.target?.cdnAccessTokens.pageInfo.endCursor) {
|
||||
return cursors;
|
||||
}
|
||||
return [...cursors, target.data?.target?.cdnAccessTokens.pageInfo.endCursor];
|
||||
});
|
||||
}}
|
||||
>
|
||||
Next Page
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{searchParams.cdn === 'create' ? (
|
||||
<CreateCDNAccessTokenModal
|
||||
onCreateCDNAccessToken={() => {
|
||||
|
|
@ -499,6 +507,6 @@ export function CDNAccessTokens(props: {
|
|||
targetId={props.targetId}
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useMutation, useQuery } from 'urql';
|
|||
import * as Yup from 'yup';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { CardDescription } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
||||
import {
|
||||
|
|
@ -26,6 +26,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -167,163 +168,168 @@ export function SchemaContracts(props: {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Schema Contracts</CardTitle>
|
||||
<CardDescription>
|
||||
Schema Contracts allow you to have separate public graphs that are a subset of the main
|
||||
graph.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink href="/management/contracts" className="text-gray-500 hover:text-gray-300">
|
||||
Learn more about Schema Contracts
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button>Create new contract</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<CreateContractDialogContent
|
||||
target={schemaContractsQuery.data?.target ?? null}
|
||||
onCreateContract={refetchQuery}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{!!contracts?.length && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Contract Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Included Tags</TableHead>
|
||||
<TableHead>Excluded Tags</TableHead>
|
||||
<TableHead>Remove unreachable API Types</TableHead>
|
||||
<TableHead className="text-right">Created at</TableHead>
|
||||
<TableHead className="text-right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{contracts.map(({ node }) => (
|
||||
<TableRow key={node.id}>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.contractName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
{node.isDisabled ? (
|
||||
<>
|
||||
<span className="text-yellow-500">Inactive</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="ml-2 text-yellow-500"
|
||||
>
|
||||
<InfoCircledIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md p-4 font-normal">
|
||||
<p>
|
||||
This Contract is no longer active and no more contract versions
|
||||
or contract checks will be published for it.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
It is not possible to enable a contract again. Please create a
|
||||
new contract instead.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Active</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size="icon-sm" className="ml-2">
|
||||
<InfoCircledIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md p-4 font-normal">
|
||||
<p>
|
||||
This Contract is active. Schema publishes and checks will
|
||||
attempt to also build the contract schema.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.includeTags?.map(tag => (
|
||||
<Badge className="mr-1" key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
)) ?? 'None'}
|
||||
</TableCell>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.excludeTags?.map(tag => (
|
||||
<Badge className="mr-1" key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
)) ?? 'None'}
|
||||
</TableCell>
|
||||
<TableCell className={cn('text-center', node.isDisabled && 'opacity-30')}>
|
||||
{node.removeUnreachableTypesFromPublicApiSchema ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<X className="size-4" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={cn('text-right', node.isDisabled && 'opacity-30')}>
|
||||
<TimeAgo date={node.createdAt} />
|
||||
</TableCell>
|
||||
<TableCell className="text-end">
|
||||
{node.viewerCanDisableContract && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="size-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => onDisable(node.id)}
|
||||
>
|
||||
Disable
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{disabledContractId && (
|
||||
<DisableContractDialog
|
||||
contractId={disabledContractId}
|
||||
onClose={() => {
|
||||
setDisabledContractId(null);
|
||||
}}
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Schema Contracts"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Schema Contracts allow you to have separate public graphs that are a subset of the
|
||||
main graph.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/contracts"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more about Schema Contracts
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button>Create new contract</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<CreateContractDialogContent
|
||||
target={schemaContractsQuery.data?.target ?? null}
|
||||
onCreateContract={refetchQuery}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
{!!contracts?.length && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Contract Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Included Tags</TableHead>
|
||||
<TableHead>Excluded Tags</TableHead>
|
||||
<TableHead>Remove unreachable API Types</TableHead>
|
||||
<TableHead className="text-right">Created at</TableHead>
|
||||
<TableHead className="text-right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{contracts.map(({ node }) => (
|
||||
<TableRow key={node.id}>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.contractName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
{node.isDisabled ? (
|
||||
<>
|
||||
<span className="text-yellow-500">Inactive</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="ml-2 text-yellow-500"
|
||||
>
|
||||
<InfoCircledIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md p-4 font-normal">
|
||||
<p>
|
||||
This Contract is no longer active and no more contract versions or
|
||||
contract checks will be published for it.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
It is not possible to enable a contract again. Please create a new
|
||||
contract instead.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Active</span>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="ghost" size="icon-sm" className="ml-2">
|
||||
<InfoCircledIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md p-4 font-normal">
|
||||
<p>
|
||||
This Contract is active. Schema publishes and checks will attempt
|
||||
to also build the contract schema.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.includeTags?.map(tag => (
|
||||
<Badge className="mr-1" key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
)) ?? 'None'}
|
||||
</TableCell>
|
||||
<TableCell className={cn(node.isDisabled && 'opacity-30')}>
|
||||
{node.excludeTags?.map(tag => (
|
||||
<Badge className="mr-1" key={tag}>
|
||||
{tag}
|
||||
</Badge>
|
||||
)) ?? 'None'}
|
||||
</TableCell>
|
||||
<TableCell className={cn('text-center', node.isDisabled && 'opacity-30')}>
|
||||
{node.removeUnreachableTypesFromPublicApiSchema ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<X className="size-4" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className={cn('text-right', node.isDisabled && 'opacity-30')}>
|
||||
<TimeAgo date={node.createdAt} />
|
||||
</TableCell>
|
||||
<TableCell className="text-end">
|
||||
{node.viewerCanDisableContract && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="size-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="text-red-500"
|
||||
onClick={() => onDisable(node.id)}
|
||||
>
|
||||
Disable
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
{disabledContractId && (
|
||||
<DisableContractDialog
|
||||
contractId={disabledContractId}
|
||||
onClose={() => {
|
||||
setDisabledContractId(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SubPageLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
73
packages/web/app/src/components/ui/page-content-layout.tsx
Normal file
73
packages/web/app/src/components/ui/page-content-layout.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CardDescription, CardTitle } from './card';
|
||||
|
||||
type NavLayoutProps = {
|
||||
children: ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const NavLayout = forwardRef<HTMLDivElement, NavLayoutProps>(({ children, ...props }, ref) => (
|
||||
<nav ref={ref} className="flex w-48 flex-col space-x-0 space-y-1" {...props}>
|
||||
{children}
|
||||
</nav>
|
||||
));
|
||||
NavLayout.displayName = 'NavLayout';
|
||||
|
||||
type PageLayoutProps = {
|
||||
children: ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const PageLayout = forwardRef<HTMLDivElement, PageLayoutProps>(({ children, ...props }, ref) => (
|
||||
<div ref={ref} className="flex flex-col gap-y-4" {...props}>
|
||||
<div className="flex flex-row gap-x-6 py-6" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
PageLayout.displayName = 'PageLayout';
|
||||
|
||||
type PageLayoutContentProps = {
|
||||
children: ReactNode;
|
||||
mainTitlePage?: string;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const PageLayoutContent = forwardRef<HTMLDivElement, PageLayoutContentProps>(
|
||||
({ children, mainTitlePage, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('grow', props.className)} {...props}>
|
||||
<h1 className="mb-2 text-2xl font-semibold">{mainTitlePage}</h1>
|
||||
{mainTitlePage ? <div className="mb-3 h-[1px] w-full bg-gray-700" /> : null}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
PageLayoutContent.displayName = 'PageLayoutContent';
|
||||
|
||||
const SubPageLayout = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
),
|
||||
);
|
||||
SubPageLayout.displayName = 'SubPageLayout';
|
||||
|
||||
type SubPageLayoutHeaderProps = {
|
||||
children?: ReactNode;
|
||||
title?: string;
|
||||
description?: string | ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const SubPageLayoutHeader = forwardRef<HTMLDivElement, SubPageLayoutHeaderProps>(({ ...props }) => (
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="space-y-1.5">
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{typeof props.description === 'string' ? (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
) : (
|
||||
props.description
|
||||
)}
|
||||
</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
));
|
||||
SubPageLayoutHeader.displayName = 'SubPageLayoutHeader';
|
||||
|
||||
export { PageLayout, NavLayout, PageLayoutContent, SubPageLayout, SubPageLayoutHeader };
|
||||
|
|
@ -158,6 +158,9 @@ export const ConnectSchemaModal = (props: {
|
|||
<span className="text-sm text-gray-500">
|
||||
To authenticate,{' '}
|
||||
<Link
|
||||
search={{
|
||||
page: 'cdn',
|
||||
}}
|
||||
variant="primary"
|
||||
className="font-bold underline"
|
||||
to="/$organizationId/$projectId/$targetId/settings"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { OrganizationMemberRolesMigration } from '@/components/organization/memb
|
|||
import { OrganizationMemberRoles } from '@/components/organization/members/roles';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { NavLayout, PageLayout, PageLayoutContent } from '@/components/ui/page-content-layout';
|
||||
import { QueryError } from '@/components/ui/query-error';
|
||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||
|
|
@ -71,49 +72,46 @@ function PageContent(props: {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex flex-row gap-x-6 py-6">
|
||||
<nav className="flex w-48 flex-col space-x-0 space-y-1">
|
||||
{subPages.map(subPage => {
|
||||
// hide migration page from non-admins
|
||||
if (subPage.key === 'migration' && !organization.me.isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={subPage.key}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
props.page === subPage.key
|
||||
? 'bg-muted hover:bg-muted'
|
||||
: 'hover:bg-transparent hover:underline',
|
||||
'justify-start',
|
||||
)}
|
||||
onClick={() => props.onPageChange(subPage.key)}
|
||||
>
|
||||
{subPage.title}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="grow">
|
||||
{props.page === 'roles' ? <OrganizationMemberRoles organization={organization} /> : null}
|
||||
{props.page === 'list' ? (
|
||||
<OrganizationMembers refetchMembers={props.refetchQuery} organization={organization} />
|
||||
) : null}
|
||||
{props.page === 'invitations' ? (
|
||||
<OrganizationInvitations
|
||||
refetchInvitations={props.refetchQuery}
|
||||
organization={organization}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'migration' && organization.me.isAdmin ? (
|
||||
<OrganizationMemberRolesMigration organization={organization} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<NavLayout>
|
||||
{subPages.map(subPage => {
|
||||
// hide migration page from non-admins
|
||||
if (subPage.key === 'migration' && !organization.me.isAdmin) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={subPage.key}
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
props.page === subPage.key
|
||||
? 'bg-muted hover:bg-muted'
|
||||
: 'hover:bg-transparent hover:underline',
|
||||
'justify-start',
|
||||
)}
|
||||
onClick={() => props.onPageChange(subPage.key)}
|
||||
>
|
||||
{subPage.title}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</NavLayout>
|
||||
<PageLayoutContent>
|
||||
{props.page === 'roles' ? <OrganizationMemberRoles organization={organization} /> : null}
|
||||
{props.page === 'list' ? (
|
||||
<OrganizationMembers refetchMembers={props.refetchQuery} organization={organization} />
|
||||
) : null}
|
||||
{props.page === 'invitations' ? (
|
||||
<OrganizationInvitations
|
||||
refetchInvitations={props.refetchQuery}
|
||||
organization={organization}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'migration' && organization.me.isAdmin ? (
|
||||
<OrganizationMemberRolesMigration organization={organization} />
|
||||
) : null}
|
||||
</PageLayoutContent>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -965,6 +965,9 @@ function LaboratoryPageContent(props: {
|
|||
projectId: props.projectId,
|
||||
targetId: props.targetId,
|
||||
}}
|
||||
search={{
|
||||
page: 'general',
|
||||
}}
|
||||
>
|
||||
<Button variant="outline" className="mr-2" size="sm">
|
||||
Connect GraphQL API Endpoint
|
||||
|
|
|
|||
|
|
@ -9,18 +9,17 @@ import { SchemaEditor } from '@/components/schema-editor';
|
|||
import { CDNAccessTokens } from '@/components/target/settings/cdn-access-tokens';
|
||||
import { SchemaContracts } from '@/components/target/settings/schema-contracts';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { CardDescription } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { DocsLink } from '@/components/ui/docs-note';
|
||||
import { Meta } from '@/components/ui/meta';
|
||||
import { Subtitle, Title } from '@/components/ui/page';
|
||||
import {
|
||||
NavLayout,
|
||||
PageLayout,
|
||||
PageLayoutContent,
|
||||
SubPageLayout,
|
||||
SubPageLayoutHeader,
|
||||
} from '@/components/ui/page-content-layout';
|
||||
import { QueryError } from '@/components/ui/query-error';
|
||||
import { TimeAgo } from '@/components/ui/time-ago';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
|
|
@ -36,6 +35,7 @@ import { ProjectType } from '@/gql/graphql';
|
|||
import { canAccessTarget, TargetAccessScope } from '@/lib/access/target';
|
||||
import { subDays } from '@/lib/date-time';
|
||||
import { useToggle } from '@/lib/hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Link, useRouter } from '@tanstack/react-router';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
|
@ -137,67 +137,69 @@ function RegistryAccessTokens(props: {
|
|||
const canManage = canAccessTarget(TargetAccessScope.TokensWrite, me);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Registry Access Tokens</CardTitle>
|
||||
<CardDescription>
|
||||
Registry Access Tokens are used to access to Hive Registry and perform actions on your
|
||||
targets/projects. In most cases, this token is used from the Hive CLI.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#registry-access-tokens"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more about Registry Access Tokens
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{canManage && (
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Button onClick={toggleModalOpen}>Create new registry token</Button>
|
||||
{checked.length === 0 ? null : (
|
||||
<Button variant="destructive" disabled={deleting} onClick={deleteTokens}>
|
||||
Delete ({checked.length || null})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
<TBody>
|
||||
{tokens?.map(token => (
|
||||
<Tr key={token.id}>
|
||||
<Td width="1">
|
||||
<Checkbox
|
||||
onCheckedChange={isChecked =>
|
||||
setChecked(
|
||||
isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id),
|
||||
)
|
||||
}
|
||||
checked={checked.includes(token.id)}
|
||||
disabled={!canManage}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{token.alias}</Td>
|
||||
<Td>{token.name}</Td>
|
||||
<Td align="right">
|
||||
{token.lastUsedAt ? (
|
||||
<>
|
||||
last used <TimeAgo date={token.lastUsedAt} />
|
||||
</>
|
||||
) : (
|
||||
'not used yet'
|
||||
)}
|
||||
</Td>
|
||||
<Td align="right">
|
||||
created <TimeAgo date={token.date} />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Registry Access Tokens"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Registry Access Tokens are used to access to Hive Registry and perform actions on your
|
||||
targets/projects. In most cases, this token is used from the Hive CLI.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#registry-access-tokens"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more about Registry Access Tokens
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{canManage && (
|
||||
<div className="my-3.5 flex justify-between">
|
||||
<Button onClick={toggleModalOpen}>Create new registry token</Button>
|
||||
{checked.length === 0 ? null : (
|
||||
<Button variant="destructive" disabled={deleting} onClick={deleteTokens}>
|
||||
Delete ({checked.length || null})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
<TBody>
|
||||
{tokens?.map(token => (
|
||||
<Tr key={token.id}>
|
||||
<Td width="1">
|
||||
<Checkbox
|
||||
onCheckedChange={isChecked =>
|
||||
setChecked(
|
||||
isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id),
|
||||
)
|
||||
}
|
||||
checked={checked.includes(token.id)}
|
||||
disabled={!canManage}
|
||||
/>
|
||||
</Td>
|
||||
<Td>{token.alias}</Td>
|
||||
<Td>{token.name}</Td>
|
||||
<Td align="right">
|
||||
{token.lastUsedAt ? (
|
||||
<>
|
||||
last used <TimeAgo date={token.lastUsedAt} />
|
||||
</>
|
||||
) : (
|
||||
'not used yet'
|
||||
)}
|
||||
</Td>
|
||||
<Td align="right">
|
||||
created <TimeAgo date={token.date} />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
{isModalOpen && (
|
||||
<CreateAccessTokenModal
|
||||
organizationId={props.organizationId}
|
||||
|
|
@ -207,7 +209,7 @@ function RegistryAccessTokens(props: {
|
|||
toggleModalOpen={toggleModalOpen}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -240,40 +242,42 @@ const ExtendBaseSchema = (props: {
|
|||
const isUnsaved = baseSchema?.trim() !== props.baseSchema?.trim();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Extend Your Schema</CardTitle>
|
||||
<CardDescription>
|
||||
Schema Extensions is pre-defined GraphQL schema that is automatically merged with your
|
||||
published schemas, before being checked and validated.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#schema-extensions"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
You can find more details and examples in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SchemaEditor
|
||||
theme="vs-dark"
|
||||
options={{ readOnly: mutation.fetching }}
|
||||
value={baseSchema}
|
||||
height={300}
|
||||
onChange={value => setBaseSchema(value ?? '')}
|
||||
/>
|
||||
{mutation.data?.updateBaseSchema.error && (
|
||||
<div className="text-red-500">{mutation.data.updateBaseSchema.error.message}</div>
|
||||
)}
|
||||
{mutation.error && (
|
||||
<div className="text-red-500">
|
||||
{mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center gap-x-3">
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Extend Your Schema"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Schema Extensions is pre-defined GraphQL schema that is automatically merged with your
|
||||
published schemas, before being checked and validated.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#schema-extensions"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
You can find more details and examples in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SchemaEditor
|
||||
theme="vs-dark"
|
||||
options={{ readOnly: mutation.fetching }}
|
||||
value={baseSchema}
|
||||
height={300}
|
||||
onChange={value => setBaseSchema(value ?? '')}
|
||||
/>
|
||||
{mutation.data?.updateBaseSchema.error && (
|
||||
<div className="text-red-500">{mutation.data.updateBaseSchema.error.message}</div>
|
||||
)}
|
||||
{mutation.error && (
|
||||
<div className="text-red-500">
|
||||
{mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Button
|
||||
className="px-5"
|
||||
disabled={mutation.fetching}
|
||||
|
|
@ -313,8 +317,8 @@ const ExtendBaseSchema = (props: {
|
|||
Reset
|
||||
</Button>
|
||||
{isUnsaved && <span className="text-sm text-green-500">Unsaved changes!</span>}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -532,46 +536,47 @@ const ConditionalBreakingChanges = (props: {
|
|||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-x-5">
|
||||
<div>Conditional Breaking Changes</div>
|
||||
{targetSettings.fetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Switch
|
||||
className="shrink-0"
|
||||
checked={isEnabled}
|
||||
onCheckedChange={async enabled => {
|
||||
await setValidation({
|
||||
input: {
|
||||
target: props.targetId,
|
||||
project: props.projectId,
|
||||
organization: props.organizationId,
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={targetValidation.fetching}
|
||||
/>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Conditional Breaking Changes can change the behavior of schema checks, based on real
|
||||
traffic data sent to Hive.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#conditional-breaking-changes"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent
|
||||
className={clsx('text-gray-300', !isEnabled && 'pointer-events-none opacity-25')}
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Conditional Breaking Changes"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Conditional Breaking Changes can change the behavior of schema checks, based on real
|
||||
traffic data sent to Hive.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#conditional-breaking-changes"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{targetSettings.fetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Switch
|
||||
className="shrink-0"
|
||||
checked={isEnabled}
|
||||
onCheckedChange={async enabled => {
|
||||
await setValidation({
|
||||
input: {
|
||||
target: props.targetId, // targetId is the target we are updating
|
||||
project: props.projectId,
|
||||
organization: props.organizationId,
|
||||
enabled,
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={targetValidation.fetching}
|
||||
/>
|
||||
)}
|
||||
</SubPageLayoutHeader>
|
||||
<div className={clsx('text-gray-300', !isEnabled && 'pointer-events-none opacity-25')}>
|
||||
<div>
|
||||
A schema change is considered as breaking only if it affects more than
|
||||
<Input
|
||||
|
|
@ -686,7 +691,7 @@ const ConditionalBreakingChanges = (props: {
|
|||
{touched.targets && errors.targets && (
|
||||
<div className="text-red-500">{errors.targets}</div>
|
||||
)}
|
||||
<div className="mt-5 space-y-2 rounded border-l-2 border-l-gray-800 bg-gray-600/10 py-2 pl-5 text-gray-400">
|
||||
<div className="mb-3 mt-5 space-y-2 rounded border-l-2 border-l-gray-800 bg-gray-600/10 py-2 pl-5 text-gray-400">
|
||||
<div>
|
||||
<div className="font-semibold">Example settings</div>
|
||||
<div className="text-sm">Removal of a field is considered breaking if</div>
|
||||
|
|
@ -705,8 +710,6 @@ const ConditionalBreakingChanges = (props: {
|
|||
- the field was requested by more than 10% of all GraphQL operations in recent 30 days
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
|
|
@ -715,8 +718,8 @@ const ConditionalBreakingChanges = (props: {
|
|||
{mutation.error.graphQLErrors[0]?.message ?? mutation.error.message}
|
||||
</span>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
@ -764,6 +767,9 @@ function TargetName(props: {
|
|||
projectId: props.projectId,
|
||||
targetId: newTargetId,
|
||||
},
|
||||
search: {
|
||||
page: subPages[0].key,
|
||||
},
|
||||
});
|
||||
} else if (result.error || result.data?.updateTargetName.error?.message) {
|
||||
toast({
|
||||
|
|
@ -776,53 +782,55 @@ function TargetName(props: {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Target Name</CardTitle>
|
||||
<CardDescription>
|
||||
Changing the name of your target will also change the slug of your target URL, and will
|
||||
invalidate any existing links to your target.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#rename-a-target"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Target Name"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Changing the name of your target will also change the slug of your target URL, and
|
||||
will invalidate any existing links to your target.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#rename-a-target"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
placeholder="Target name"
|
||||
name="name"
|
||||
value={values.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.name && !!errors.name}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
placeholder="Target name"
|
||||
name="name"
|
||||
value={values.name}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.name && !!errors.name}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{touched.name && (errors.name || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message}
|
||||
</div>
|
||||
)}
|
||||
{mutation.data?.updateTargetName.error?.inputErrors?.name && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data.updateTargetName.error.inputErrors.name}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
{touched.name && (errors.name || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message}
|
||||
</div>
|
||||
)}
|
||||
{mutation.data?.updateTargetName.error?.inputErrors?.name && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data.updateTargetName.error.inputErrors.name}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Card>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -891,56 +899,58 @@ function GraphQLEndpointUrl(props: {
|
|||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>GraphQL Endpoint URL</CardTitle>
|
||||
<CardDescription>
|
||||
The endpoint url will be used for querying the target from the{' '}
|
||||
<Link
|
||||
to="/$organizationId/$projectId/$targetId/laboratory"
|
||||
params={{
|
||||
organizationId: props.organizationId,
|
||||
projectId: props.projectId,
|
||||
targetId: props.targetId,
|
||||
}}
|
||||
>
|
||||
Hive Laboratory
|
||||
</Link>
|
||||
.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="GraphQL Endpoint URL"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
The endpoint url will be used for querying the target from the{' '}
|
||||
<Link
|
||||
to="/$organizationId/$projectId/$targetId/laboratory"
|
||||
params={{
|
||||
organizationId: props.organizationId,
|
||||
projectId: props.projectId,
|
||||
targetId: props.targetId,
|
||||
}}
|
||||
>
|
||||
Hive Laboratory
|
||||
</Link>
|
||||
.
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
placeholder="Endpoint Url"
|
||||
name="graphqlEndpointUrl"
|
||||
value={values.graphqlEndpointUrl}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.graphqlEndpointUrl && !!errors.graphqlEndpointUrl}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<Input
|
||||
placeholder="Endpoint Url"
|
||||
name="graphqlEndpointUrl"
|
||||
value={values.graphqlEndpointUrl}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={isSubmitting}
|
||||
isInvalid={touched.graphqlEndpointUrl && !!errors.graphqlEndpointUrl}
|
||||
className="w-96"
|
||||
/>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.graphqlEndpointUrl ??
|
||||
mutation.error?.graphQLErrors[0]?.message ??
|
||||
mutation.error?.message}
|
||||
</div>
|
||||
{touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{errors.graphqlEndpointUrl ??
|
||||
mutation.error?.graphQLErrors[0]?.message ??
|
||||
mutation.error?.message}
|
||||
</div>
|
||||
)}
|
||||
{mutation.data?.updateTargetGraphQLEndpointUrl.error && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data.updateTargetGraphQLEndpointUrl.error.message}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
{mutation.data?.updateTargetGraphQLEndpointUrl.error && (
|
||||
<div className="mt-2 text-red-500">
|
||||
{mutation.data.updateTargetGraphQLEndpointUrl.error.message}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Card>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -991,29 +1001,30 @@ function TargetDelete(props: { organizationId: string; projectId: string; target
|
|||
const [isModalOpen, toggleModalOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="mb-10">
|
||||
<CardHeader>
|
||||
<CardTitle>Delete Target</CardTitle>
|
||||
<CardDescription>
|
||||
Deleting an project also delete all schemas and data associated with it.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#delete-a-target"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
<strong>This action is not reversible!</strong> You can find more information about
|
||||
this process in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button variant="destructive" onClick={toggleModalOpen}>
|
||||
Delete Target
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
title="Delete Target"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Deleting an project also delete all schemas and data associated with it.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
href="/management/targets#delete-a-target"
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
<strong>This action is not reversible!</strong> You can find more information about
|
||||
this process in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="destructive" onClick={toggleModalOpen}>
|
||||
Delete Target
|
||||
</Button>
|
||||
|
||||
<DeleteTargetModal
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
|
|
@ -1021,7 +1032,7 @@ function TargetDelete(props: { organizationId: string; projectId: string; target
|
|||
isOpen={isModalOpen}
|
||||
toggleModalOpen={toggleModalOpen}
|
||||
/>
|
||||
</>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1052,11 +1063,42 @@ const TargetSettingsPageQuery = graphql(`
|
|||
}
|
||||
`);
|
||||
|
||||
const subPages = [
|
||||
{
|
||||
key: 'general',
|
||||
title: 'General',
|
||||
},
|
||||
{
|
||||
key: 'cdn',
|
||||
title: 'CDN Tokens',
|
||||
},
|
||||
{
|
||||
key: 'registry-token',
|
||||
title: 'Registry Tokens',
|
||||
},
|
||||
{
|
||||
key: 'breaking-changes',
|
||||
title: 'Breaking Changes',
|
||||
},
|
||||
{
|
||||
key: 'base-schema',
|
||||
title: 'Base Schema',
|
||||
},
|
||||
{
|
||||
key: 'schema-contracts',
|
||||
title: 'Schema Contracts',
|
||||
},
|
||||
] as const;
|
||||
|
||||
type SubPage = (typeof subPages)[number]['key'];
|
||||
|
||||
function TargetSettingsContent(props: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
targetId: string;
|
||||
page?: SubPage;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [query] = useQuery({
|
||||
query: TargetSettingsPageQuery,
|
||||
variables: {
|
||||
|
|
@ -1073,6 +1115,7 @@ function TargetSettingsContent(props: {
|
|||
TargetSettingsPage_OrganizationFragment,
|
||||
currentOrganization,
|
||||
);
|
||||
|
||||
const targetForSettings = useFragment(TargetSettingsPage_TargetFragment, currentTarget);
|
||||
|
||||
const canAccessTokens = canAccessTarget(
|
||||
|
|
@ -1092,66 +1135,103 @@ function TargetSettingsContent(props: {
|
|||
organizationId={props.organizationId}
|
||||
page={Page.Settings}
|
||||
>
|
||||
<div className="py-6">
|
||||
<Title>Settings</Title>
|
||||
<Subtitle>Manage your target settings.</Subtitle>
|
||||
</div>
|
||||
{currentOrganization && currentProject && currentTarget && organizationForSettings ? (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<TargetName
|
||||
targetName={currentTarget.name}
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
/>
|
||||
<GraphQLEndpointUrl
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
graphqlEndpointUrl={currentTarget.graphqlEndpointUrl ?? null}
|
||||
/>
|
||||
{canAccessTokens && (
|
||||
<RegistryAccessTokens
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
me={organizationForSettings.me}
|
||||
/>
|
||||
)}
|
||||
{canAccessTokens && (
|
||||
<CDNAccessTokens
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
me={organizationForSettings.me}
|
||||
/>
|
||||
)}
|
||||
{currentProject.type === ProjectType.Federation && (
|
||||
<SchemaContracts
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
/>
|
||||
)}
|
||||
<ConditionalBreakingChanges
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
/>
|
||||
<ExtendBaseSchema
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
baseSchema={targetForSettings?.baseSchema ?? ''}
|
||||
/>
|
||||
{canDelete && (
|
||||
<TargetDelete
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<PageLayout>
|
||||
<NavLayout>
|
||||
{subPages.map(subPage => {
|
||||
if (
|
||||
subPage.key === 'schema-contracts' &&
|
||||
currentProject.type !== ProjectType.Federation
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={subPage.key}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search: {
|
||||
page: subPage.key,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
props.page === subPage.key
|
||||
? 'bg-muted hover:bg-muted'
|
||||
: 'hover:bg-transparent hover:underline',
|
||||
'w-full justify-start text-left',
|
||||
)}
|
||||
>
|
||||
{subPage.title}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</NavLayout>
|
||||
<PageLayoutContent>
|
||||
{currentOrganization && currentProject && currentTarget && organizationForSettings ? (
|
||||
<div className="space-y-12">
|
||||
{props.page === 'general' ? (
|
||||
<>
|
||||
<TargetName
|
||||
targetName={currentTarget.name}
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
/>
|
||||
<GraphQLEndpointUrl
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
graphqlEndpointUrl={currentTarget.graphqlEndpointUrl ?? null}
|
||||
/>
|
||||
{canDelete && (
|
||||
<TargetDelete
|
||||
targetId={currentTarget.cleanId}
|
||||
projectId={currentProject.cleanId}
|
||||
organizationId={currentOrganization.cleanId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{props.page === 'cdn' && canAccessTokens ? (
|
||||
<CDNAccessTokens
|
||||
me={organizationForSettings.me}
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'registry-token' && canAccessTokens ? (
|
||||
<RegistryAccessTokens
|
||||
me={organizationForSettings.me}
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'breaking-changes' ? (
|
||||
<ConditionalBreakingChanges
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'base-schema' ? (
|
||||
<ExtendBaseSchema
|
||||
baseSchema={targetForSettings?.baseSchema ?? ''}
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
/>
|
||||
) : null}
|
||||
{props.page === 'schema-contracts' ? (
|
||||
<SchemaContracts organizationId="" projectId="" targetId="" />
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</PageLayoutContent>
|
||||
</PageLayout>
|
||||
) : null}
|
||||
</TargetLayout>
|
||||
);
|
||||
|
|
@ -1161,11 +1241,17 @@ export function TargetSettingsPage(props: {
|
|||
organizationId: string;
|
||||
projectId: string;
|
||||
targetId: string;
|
||||
page?: SubPage;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Settings" />
|
||||
<TargetSettingsContent {...props} />
|
||||
<TargetSettingsContent
|
||||
organizationId={props.organizationId}
|
||||
projectId={props.projectId}
|
||||
targetId={props.targetId}
|
||||
page={props.page}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,16 +383,36 @@ const targetIndexRoute = createRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const TargetSettingRouteSearch = z.object({
|
||||
page: z
|
||||
.enum([
|
||||
'general',
|
||||
'cdn',
|
||||
'registry-token',
|
||||
'breaking-changes',
|
||||
'base-schema',
|
||||
'schema-contracts',
|
||||
])
|
||||
.default('general')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const targetSettingsRoute = createRoute({
|
||||
getParentRoute: () => targetRoute,
|
||||
path: 'settings',
|
||||
validateSearch(search) {
|
||||
return TargetSettingRouteSearch.parse(search);
|
||||
},
|
||||
component: function TargetSettingsRoute() {
|
||||
const { organizationId, projectId, targetId } = targetSettingsRoute.useParams();
|
||||
const { page } = targetSettingsRoute.useSearch();
|
||||
|
||||
return (
|
||||
<TargetSettingsPage
|
||||
organizationId={organizationId}
|
||||
projectId={projectId}
|
||||
targetId={targetId}
|
||||
page={page}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
59
packages/web/app/src/stories/sub-page-layout.stories.tsx
Normal file
59
packages/web/app/src/stories/sub-page-layout.stories.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { NavLayout, PageLayout, PageLayoutContent } from '@/components/ui/page-content-layout';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof PageLayout> = {
|
||||
title: 'Layout/Sub page layout',
|
||||
component: PageLayout,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
const subPages = [
|
||||
{ key: 'general', title: 'General' },
|
||||
{ key: 'cdn', title: 'CDN Tokens' },
|
||||
{ key: 'registry-token', title: 'Registry Tokens' },
|
||||
{ key: 'breaking-changes', title: 'Breaking Changes' },
|
||||
{ key: 'base-schema', title: 'Base Schema' },
|
||||
{ key: 'schema-contracts', title: 'Schema Contracts' },
|
||||
];
|
||||
|
||||
const Template: StoryObj<typeof PageLayout> = {
|
||||
render: () => {
|
||||
const [page, setPage] = useState('general');
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<NavLayout>
|
||||
{subPages.map(subPage => {
|
||||
return (
|
||||
<Button
|
||||
key={subPage.key}
|
||||
variant="ghost"
|
||||
onClick={() => setPage(subPage.key)}
|
||||
className={
|
||||
page === subPage.key
|
||||
? 'bg-muted hover:bg-muted'
|
||||
: 'hover:bg-transparent hover:underline'
|
||||
}
|
||||
>
|
||||
{subPage.title}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</NavLayout>
|
||||
<PageLayoutContent mainTitlePage={subPages.find(subPage => subPage.key === page)?.title}>
|
||||
{page === 'general' && <div className="flex flex-col gap-10">General</div>}
|
||||
{page === 'cdn' && <div>CDN Tokens</div>}
|
||||
{page === 'registry-token' && <div>Registry Tokens</div>}
|
||||
{page === 'breaking-changes' && <div>Breaking Changes</div>}
|
||||
{page === 'base-schema' && <div>Base Schema</div>}
|
||||
{page === 'schema-contracts' && <div>Schema Contracts</div>}
|
||||
</PageLayoutContent>
|
||||
</PageLayout>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = Template;
|
||||
Loading…
Reference in a new issue