mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +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 = {
|
const preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
default: 'dark',
|
default: 'black',
|
||||||
},
|
},
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
controls: {
|
controls: {
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,7 @@ export const TargetLayout = ({
|
||||||
projectId: props.projectId,
|
projectId: props.projectId,
|
||||||
targetId: props.targetId,
|
targetId: props.targetId,
|
||||||
}}
|
}}
|
||||||
|
search={{ page: 'general' }}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -32,6 +31,7 @@ import {
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||||
import { useClipboard } from '@/lib/hooks';
|
import { useClipboard } from '@/lib/hooks';
|
||||||
|
|
@ -428,21 +428,16 @@ export function OrganizationInvitations(props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<SubPageLayout>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<SubPageLayoutHeader
|
||||||
<div className="space-y-2">
|
title="Pending invitations"
|
||||||
<CardTitle>Pending invitations</CardTitle>
|
description="Active invitations to join this organization. Invitations expire after 7 days."
|
||||||
<CardDescription>
|
>
|
||||||
Active invitations to join this organization. Invitations expire after 7 days.
|
<MemberInvitationButton
|
||||||
</CardDescription>
|
refetchInvitations={props.refetchInvitations}
|
||||||
</div>
|
organization={organization}
|
||||||
<div>
|
/>
|
||||||
<MemberInvitationButton
|
</SubPageLayoutHeader>
|
||||||
refetchInvitations={props.refetchInvitations}
|
|
||||||
organization={organization}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{organization.invitations.nodes.length > 0 ? (
|
{organization.invitations.nodes.length > 0 ? (
|
||||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -476,6 +471,6 @@ export function OrganizationInvitations(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import { ChangePermissionsModal } from '@/components/v2/modals';
|
import { ChangePermissionsModal } from '@/components/v2/modals';
|
||||||
|
|
@ -526,21 +526,16 @@ export function OrganizationMembers(props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<SubPageLayout>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<SubPageLayoutHeader
|
||||||
<div className="space-y-2">
|
title="List of organization members"
|
||||||
<CardTitle>List of organization members</CardTitle>
|
description="Manage the members of your organization and their permissions."
|
||||||
<CardDescription>
|
>
|
||||||
Manage the members of your organization and their permissions.
|
<MemberInvitationButton
|
||||||
</CardDescription>
|
refetchInvitations={props.refetchMembers}
|
||||||
</div>
|
organization={organization}
|
||||||
<div>
|
/>
|
||||||
<MemberInvitationButton
|
</SubPageLayoutHeader>
|
||||||
refetchInvitations={props.refetchMembers}
|
|
||||||
organization={organization}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -589,6 +584,6 @@ export function OrganizationMembers(props: {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
import { CardDescription } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -30,6 +30,7 @@ import { ProductUpdatesLink } from '@/components/ui/docs-note';
|
||||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
|
@ -849,22 +850,26 @@ export function OrganizationMemberRolesMigration(props: {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<SubPageLayout>
|
||||||
<div className="space-y-2">
|
<SubPageLayoutHeader
|
||||||
<CardTitle>Migration Wizard</CardTitle>
|
title="Migration Wizard"
|
||||||
<CardDescription>
|
description={
|
||||||
This wizard will help you migrate your organization's members to the new permissions
|
<>
|
||||||
system.
|
<CardDescription>
|
||||||
</CardDescription>
|
This wizard will help you migrate your organization's members to the new permissions
|
||||||
<CardDescription>
|
system.
|
||||||
Members are grouped by their access scopes.
|
</CardDescription>
|
||||||
<br /> You can choose to migrate all members from each group to a new role or assign them
|
<CardDescription>
|
||||||
to an existing role.
|
Members are grouped by their access scopes.
|
||||||
</CardDescription>
|
<br /> You can choose to migrate all members from each group to a new role or assign
|
||||||
<ProductUpdatesLink href="2023-12-05-member-roles">
|
them to an existing role.
|
||||||
Read "Introducing Member Roles" product update to learn more.
|
</CardDescription>
|
||||||
</ProductUpdatesLink>
|
<ProductUpdatesLink href="2023-12-05-member-roles">
|
||||||
</div>
|
Read "Introducing Member Roles" product update to learn more.
|
||||||
|
</ProductUpdatesLink>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{organization.unassignedMembersToMigrate.length > 0 ? (
|
{organization.unassignedMembersToMigrate.length > 0 ? (
|
||||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -900,6 +905,6 @@ export function OrganizationMemberRolesMigration(props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { CardDescription, CardTitle } from '@/components/ui/card';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -40,6 +39,7 @@ import {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
@ -894,21 +894,16 @@ export function OrganizationMemberRoles(props: {
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
) : null}
|
) : null}
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
<div className="space-y-6">
|
<SubPageLayout>
|
||||||
<div className="flex flex-row items-center justify-between">
|
<SubPageLayoutHeader
|
||||||
<div className="space-y-2">
|
title="List of roles"
|
||||||
<CardTitle>List of roles</CardTitle>
|
description="Manage the roles that can be assigned to members of this organization."
|
||||||
<CardDescription>
|
>
|
||||||
Manage the roles that can be assigned to members of this organization.
|
<OrganizationMemberRoleCreateButton
|
||||||
</CardDescription>
|
me={organization.me}
|
||||||
</div>
|
organizationCleanId={organization.cleanId}
|
||||||
<div>
|
/>
|
||||||
<OrganizationMemberRoleCreateButton
|
</SubPageLayoutHeader>
|
||||||
me={organization.me}
|
|
||||||
organizationCleanId={organization.cleanId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
<table className="w-full table-auto divide-y-[1px] divide-gray-500/20">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -930,7 +925,7 @@ export function OrganizationMemberRoles(props: {
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</SubPageLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import { useMutation, useQuery } from 'urql';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Button } from '@/components/ui/button';
|
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 {
|
import {
|
||||||
DocsLink,
|
DocsLink,
|
||||||
Heading,
|
Heading,
|
||||||
|
|
@ -354,7 +355,9 @@ export function CDNAccessTokens(props: {
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
void router.navigate({
|
void router.navigate({
|
||||||
search: {},
|
search: {
|
||||||
|
page: 'cdn',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -375,107 +378,112 @@ export function CDNAccessTokens(props: {
|
||||||
const canManage = canAccessTarget(TargetAccessScope.Settings, me);
|
const canManage = canAccessTarget(TargetAccessScope.Settings, me);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle id="cdn-access-tokens">CDN Access Token</CardTitle>
|
title="CDN Access Token"
|
||||||
<CardDescription>
|
description={
|
||||||
CDN Access Tokens are used to access to Hive High-Availability CDN and read your schema
|
<>
|
||||||
artifacts.
|
<CardDescription>
|
||||||
</CardDescription>
|
CDN Access Tokens are used to access to Hive High-Availability CDN and read your
|
||||||
<CardDescription>
|
schema artifacts.
|
||||||
<DocsLink
|
</CardDescription>
|
||||||
href="/management/targets#cdn-access-tokens"
|
<CardDescription>
|
||||||
className="text-gray-500 hover:text-gray-300"
|
<DocsLink
|
||||||
>
|
href="/management/targets#cdn-access-tokens"
|
||||||
Learn more about CDN Access Tokens
|
className="text-gray-500 hover:text-gray-300"
|
||||||
</DocsLink>
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{canManage && (
|
|
||||||
<div className="my-3.5 flex justify-between">
|
|
||||||
<Button asChild>
|
|
||||||
<Link
|
|
||||||
search={{
|
|
||||||
cdn: 'create',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Create new CDN token
|
Learn more about CDN Access Tokens
|
||||||
</Link>
|
</DocsLink>
|
||||||
</Button>
|
</CardDescription>
|
||||||
</div>
|
</>
|
||||||
)}
|
}
|
||||||
<Table>
|
/>
|
||||||
<TBody>
|
{canManage && (
|
||||||
{target?.data?.target?.cdnAccessTokens.edges?.map(edge => {
|
<div className="my-3.5 flex justify-between">
|
||||||
const node = useFragment(CDNAccessTokeRowFragment, edge.node);
|
<Button asChild>
|
||||||
|
<Link
|
||||||
return (
|
search={{
|
||||||
<Tr key={node.id}>
|
page: 'cdn',
|
||||||
<Td>
|
cdn: 'create',
|
||||||
{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);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Previous Page
|
Create new CDN token
|
||||||
</Button>
|
</Link>
|
||||||
) : null}
|
</Button>
|
||||||
{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>
|
</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' ? (
|
{searchParams.cdn === 'create' ? (
|
||||||
<CreateCDNAccessTokenModal
|
<CreateCDNAccessTokenModal
|
||||||
onCreateCDNAccessToken={() => {
|
onCreateCDNAccessToken={() => {
|
||||||
|
|
@ -499,6 +507,6 @@ export function CDNAccessTokens(props: {
|
||||||
targetId={props.targetId}
|
targetId={props.targetId}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { useMutation, useQuery } from 'urql';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
import { Command, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
||||||
import {
|
import {
|
||||||
|
|
@ -26,6 +26,7 @@ import {
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
|
@ -167,163 +168,168 @@ export function SchemaContracts(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle>Schema Contracts</CardTitle>
|
title="Schema Contracts"
|
||||||
<CardDescription>
|
description={
|
||||||
Schema Contracts allow you to have separate public graphs that are a subset of the main
|
<>
|
||||||
graph.
|
<CardDescription>
|
||||||
</CardDescription>
|
Schema Contracts allow you to have separate public graphs that are a subset of the
|
||||||
<CardDescription>
|
main graph.
|
||||||
<DocsLink href="/management/contracts" className="text-gray-500 hover:text-gray-300">
|
</CardDescription>
|
||||||
Learn more about Schema Contracts
|
<CardDescription>
|
||||||
</DocsLink>
|
<DocsLink
|
||||||
</CardDescription>
|
href="/management/contracts"
|
||||||
</CardHeader>
|
className="text-gray-500 hover:text-gray-300"
|
||||||
<CardContent>
|
>
|
||||||
<div className="my-3.5 flex justify-between">
|
Learn more about Schema Contracts
|
||||||
<Dialog>
|
</DocsLink>
|
||||||
<DialogTrigger>
|
</CardDescription>
|
||||||
<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);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<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">
|
<span className="text-sm text-gray-500">
|
||||||
To authenticate,{' '}
|
To authenticate,{' '}
|
||||||
<Link
|
<Link
|
||||||
|
search={{
|
||||||
|
page: 'cdn',
|
||||||
|
}}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="font-bold underline"
|
className="font-bold underline"
|
||||||
to="/$organizationId/$projectId/$targetId/settings"
|
to="/$organizationId/$projectId/$targetId/settings"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { OrganizationMemberRolesMigration } from '@/components/organization/memb
|
||||||
import { OrganizationMemberRoles } from '@/components/organization/members/roles';
|
import { OrganizationMemberRoles } from '@/components/organization/members/roles';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Meta } from '@/components/ui/meta';
|
import { Meta } from '@/components/ui/meta';
|
||||||
|
import { NavLayout, PageLayout, PageLayoutContent } from '@/components/ui/page-content-layout';
|
||||||
import { QueryError } from '@/components/ui/query-error';
|
import { QueryError } from '@/components/ui/query-error';
|
||||||
import { FragmentType, graphql, useFragment } from '@/gql';
|
import { FragmentType, graphql, useFragment } from '@/gql';
|
||||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
||||||
|
|
@ -71,49 +72,46 @@ function PageContent(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-4">
|
<PageLayout>
|
||||||
<div className="flex flex-row gap-x-6 py-6">
|
<NavLayout>
|
||||||
<nav className="flex w-48 flex-col space-x-0 space-y-1">
|
{subPages.map(subPage => {
|
||||||
{subPages.map(subPage => {
|
// hide migration page from non-admins
|
||||||
// hide migration page from non-admins
|
if (subPage.key === 'migration' && !organization.me.isAdmin) {
|
||||||
if (subPage.key === 'migration' && !organization.me.isAdmin) {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
return (
|
||||||
|
<Button
|
||||||
return (
|
key={subPage.key}
|
||||||
<Button
|
variant="ghost"
|
||||||
key={subPage.key}
|
className={cn(
|
||||||
variant="ghost"
|
props.page === subPage.key
|
||||||
className={cn(
|
? 'bg-muted hover:bg-muted'
|
||||||
props.page === subPage.key
|
: 'hover:bg-transparent hover:underline',
|
||||||
? 'bg-muted hover:bg-muted'
|
'justify-start',
|
||||||
: 'hover:bg-transparent hover:underline',
|
)}
|
||||||
'justify-start',
|
onClick={() => props.onPageChange(subPage.key)}
|
||||||
)}
|
>
|
||||||
onClick={() => props.onPageChange(subPage.key)}
|
{subPage.title}
|
||||||
>
|
</Button>
|
||||||
{subPage.title}
|
);
|
||||||
</Button>
|
})}
|
||||||
);
|
</NavLayout>
|
||||||
})}
|
<PageLayoutContent>
|
||||||
</nav>
|
{props.page === 'roles' ? <OrganizationMemberRoles organization={organization} /> : null}
|
||||||
<div className="grow">
|
{props.page === 'list' ? (
|
||||||
{props.page === 'roles' ? <OrganizationMemberRoles organization={organization} /> : null}
|
<OrganizationMembers refetchMembers={props.refetchQuery} organization={organization} />
|
||||||
{props.page === 'list' ? (
|
) : null}
|
||||||
<OrganizationMembers refetchMembers={props.refetchQuery} organization={organization} />
|
{props.page === 'invitations' ? (
|
||||||
) : null}
|
<OrganizationInvitations
|
||||||
{props.page === 'invitations' ? (
|
refetchInvitations={props.refetchQuery}
|
||||||
<OrganizationInvitations
|
organization={organization}
|
||||||
refetchInvitations={props.refetchQuery}
|
/>
|
||||||
organization={organization}
|
) : null}
|
||||||
/>
|
{props.page === 'migration' && organization.me.isAdmin ? (
|
||||||
) : null}
|
<OrganizationMemberRolesMigration organization={organization} />
|
||||||
{props.page === 'migration' && organization.me.isAdmin ? (
|
) : null}
|
||||||
<OrganizationMemberRolesMigration organization={organization} />
|
</PageLayoutContent>
|
||||||
) : null}
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -965,6 +965,9 @@ function LaboratoryPageContent(props: {
|
||||||
projectId: props.projectId,
|
projectId: props.projectId,
|
||||||
targetId: props.targetId,
|
targetId: props.targetId,
|
||||||
}}
|
}}
|
||||||
|
search={{
|
||||||
|
page: 'general',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="outline" className="mr-2" size="sm">
|
<Button variant="outline" className="mr-2" size="sm">
|
||||||
Connect GraphQL API Endpoint
|
Connect GraphQL API Endpoint
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,17 @@ import { SchemaEditor } from '@/components/schema-editor';
|
||||||
import { CDNAccessTokens } from '@/components/target/settings/cdn-access-tokens';
|
import { CDNAccessTokens } from '@/components/target/settings/cdn-access-tokens';
|
||||||
import { SchemaContracts } from '@/components/target/settings/schema-contracts';
|
import { SchemaContracts } from '@/components/target/settings/schema-contracts';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import { CardDescription } from '@/components/ui/card';
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { DocsLink } from '@/components/ui/docs-note';
|
import { DocsLink } from '@/components/ui/docs-note';
|
||||||
import { Meta } from '@/components/ui/meta';
|
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 { QueryError } from '@/components/ui/query-error';
|
||||||
import { TimeAgo } from '@/components/ui/time-ago';
|
import { TimeAgo } from '@/components/ui/time-ago';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
|
@ -36,6 +35,7 @@ import { ProjectType } from '@/gql/graphql';
|
||||||
import { canAccessTarget, TargetAccessScope } from '@/lib/access/target';
|
import { canAccessTarget, TargetAccessScope } from '@/lib/access/target';
|
||||||
import { subDays } from '@/lib/date-time';
|
import { subDays } from '@/lib/date-time';
|
||||||
import { useToggle } from '@/lib/hooks';
|
import { useToggle } from '@/lib/hooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { Link, useRouter } from '@tanstack/react-router';
|
import { Link, useRouter } from '@tanstack/react-router';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|
@ -137,67 +137,69 @@ function RegistryAccessTokens(props: {
|
||||||
const canManage = canAccessTarget(TargetAccessScope.TokensWrite, me);
|
const canManage = canAccessTarget(TargetAccessScope.TokensWrite, me);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle>Registry Access Tokens</CardTitle>
|
title="Registry Access Tokens"
|
||||||
<CardDescription>
|
description={
|
||||||
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>
|
Registry Access Tokens are used to access to Hive Registry and perform actions on your
|
||||||
<CardDescription>
|
targets/projects. In most cases, this token is used from the Hive CLI.
|
||||||
<DocsLink
|
</CardDescription>
|
||||||
href="/management/targets#registry-access-tokens"
|
<CardDescription>
|
||||||
className="text-gray-500 hover:text-gray-300"
|
<DocsLink
|
||||||
>
|
href="/management/targets#registry-access-tokens"
|
||||||
Learn more about Registry Access Tokens
|
className="text-gray-500 hover:text-gray-300"
|
||||||
</DocsLink>
|
>
|
||||||
</CardDescription>
|
Learn more about Registry Access Tokens
|
||||||
</CardHeader>
|
</DocsLink>
|
||||||
<CardContent>
|
</CardDescription>
|
||||||
{canManage && (
|
</>
|
||||||
<div className="my-3.5 flex justify-between">
|
}
|
||||||
<Button onClick={toggleModalOpen}>Create new registry token</Button>
|
/>
|
||||||
{checked.length === 0 ? null : (
|
{canManage && (
|
||||||
<Button variant="destructive" disabled={deleting} onClick={deleteTokens}>
|
<div className="my-3.5 flex justify-between">
|
||||||
Delete ({checked.length || null})
|
<Button onClick={toggleModalOpen}>Create new registry token</Button>
|
||||||
</Button>
|
{checked.length === 0 ? null : (
|
||||||
)}
|
<Button variant="destructive" disabled={deleting} onClick={deleteTokens}>
|
||||||
</div>
|
Delete ({checked.length || null})
|
||||||
)}
|
</Button>
|
||||||
<Table>
|
)}
|
||||||
<TBody>
|
</div>
|
||||||
{tokens?.map(token => (
|
)}
|
||||||
<Tr key={token.id}>
|
<Table>
|
||||||
<Td width="1">
|
<TBody>
|
||||||
<Checkbox
|
{tokens?.map(token => (
|
||||||
onCheckedChange={isChecked =>
|
<Tr key={token.id}>
|
||||||
setChecked(
|
<Td width="1">
|
||||||
isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id),
|
<Checkbox
|
||||||
)
|
onCheckedChange={isChecked =>
|
||||||
}
|
setChecked(
|
||||||
checked={checked.includes(token.id)}
|
isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id),
|
||||||
disabled={!canManage}
|
)
|
||||||
/>
|
}
|
||||||
</Td>
|
checked={checked.includes(token.id)}
|
||||||
<Td>{token.alias}</Td>
|
disabled={!canManage}
|
||||||
<Td>{token.name}</Td>
|
/>
|
||||||
<Td align="right">
|
</Td>
|
||||||
{token.lastUsedAt ? (
|
<Td>{token.alias}</Td>
|
||||||
<>
|
<Td>{token.name}</Td>
|
||||||
last used <TimeAgo date={token.lastUsedAt} />
|
<Td align="right">
|
||||||
</>
|
{token.lastUsedAt ? (
|
||||||
) : (
|
<>
|
||||||
'not used yet'
|
last used <TimeAgo date={token.lastUsedAt} />
|
||||||
)}
|
</>
|
||||||
</Td>
|
) : (
|
||||||
<Td align="right">
|
'not used yet'
|
||||||
created <TimeAgo date={token.date} />
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
<Td align="right">
|
||||||
))}
|
created <TimeAgo date={token.date} />
|
||||||
</TBody>
|
</Td>
|
||||||
</Table>
|
</Tr>
|
||||||
</CardContent>
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<CreateAccessTokenModal
|
<CreateAccessTokenModal
|
||||||
organizationId={props.organizationId}
|
organizationId={props.organizationId}
|
||||||
|
|
@ -207,7 +209,7 @@ function RegistryAccessTokens(props: {
|
||||||
toggleModalOpen={toggleModalOpen}
|
toggleModalOpen={toggleModalOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,40 +242,42 @@ const ExtendBaseSchema = (props: {
|
||||||
const isUnsaved = baseSchema?.trim() !== props.baseSchema?.trim();
|
const isUnsaved = baseSchema?.trim() !== props.baseSchema?.trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle>Extend Your Schema</CardTitle>
|
title="Extend Your Schema"
|
||||||
<CardDescription>
|
description={
|
||||||
Schema Extensions is pre-defined GraphQL schema that is automatically merged with your
|
<>
|
||||||
published schemas, before being checked and validated.
|
<CardDescription>
|
||||||
</CardDescription>
|
Schema Extensions is pre-defined GraphQL schema that is automatically merged with your
|
||||||
<CardDescription>
|
published schemas, before being checked and validated.
|
||||||
<DocsLink
|
</CardDescription>
|
||||||
href="/management/targets#schema-extensions"
|
<CardDescription>
|
||||||
className="text-gray-500 hover:text-gray-300"
|
<DocsLink
|
||||||
>
|
href="/management/targets#schema-extensions"
|
||||||
You can find more details and examples in the documentation
|
className="text-gray-500 hover:text-gray-300"
|
||||||
</DocsLink>
|
>
|
||||||
</CardDescription>
|
You can find more details and examples in the documentation
|
||||||
</CardHeader>
|
</DocsLink>
|
||||||
<CardContent>
|
</CardDescription>
|
||||||
<SchemaEditor
|
</>
|
||||||
theme="vs-dark"
|
}
|
||||||
options={{ readOnly: mutation.fetching }}
|
/>
|
||||||
value={baseSchema}
|
<SchemaEditor
|
||||||
height={300}
|
theme="vs-dark"
|
||||||
onChange={value => setBaseSchema(value ?? '')}
|
options={{ readOnly: mutation.fetching }}
|
||||||
/>
|
value={baseSchema}
|
||||||
{mutation.data?.updateBaseSchema.error && (
|
height={300}
|
||||||
<div className="text-red-500">{mutation.data.updateBaseSchema.error.message}</div>
|
onChange={value => setBaseSchema(value ?? '')}
|
||||||
)}
|
/>
|
||||||
{mutation.error && (
|
{mutation.data?.updateBaseSchema.error && (
|
||||||
<div className="text-red-500">
|
<div className="text-red-500">{mutation.data.updateBaseSchema.error.message}</div>
|
||||||
{mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message}
|
)}
|
||||||
</div>
|
{mutation.error && (
|
||||||
)}
|
<div className="text-red-500">
|
||||||
</CardContent>
|
{mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message}
|
||||||
<CardFooter className="flex items-center gap-x-3">
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
<Button
|
<Button
|
||||||
className="px-5"
|
className="px-5"
|
||||||
disabled={mutation.fetching}
|
disabled={mutation.fetching}
|
||||||
|
|
@ -313,8 +317,8 @@ const ExtendBaseSchema = (props: {
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
{isUnsaved && <span className="text-sm text-green-500">Unsaved changes!</span>}
|
{isUnsaved && <span className="text-sm text-green-500">Unsaved changes!</span>}
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -532,46 +536,47 @@ const ConditionalBreakingChanges = (props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle className="flex items-center justify-between gap-x-5">
|
title="Conditional Breaking Changes"
|
||||||
<div>Conditional Breaking Changes</div>
|
description={
|
||||||
{targetSettings.fetching ? (
|
<>
|
||||||
<Spinner />
|
<CardDescription>
|
||||||
) : (
|
Conditional Breaking Changes can change the behavior of schema checks, based on real
|
||||||
<Switch
|
traffic data sent to Hive.
|
||||||
className="shrink-0"
|
</CardDescription>
|
||||||
checked={isEnabled}
|
<CardDescription>
|
||||||
onCheckedChange={async enabled => {
|
<DocsLink
|
||||||
await setValidation({
|
href="/management/targets#conditional-breaking-changes"
|
||||||
input: {
|
className="text-gray-500 hover:text-gray-300"
|
||||||
target: props.targetId,
|
>
|
||||||
project: props.projectId,
|
Learn more
|
||||||
organization: props.organizationId,
|
</DocsLink>
|
||||||
enabled,
|
</CardDescription>
|
||||||
},
|
</>
|
||||||
});
|
}
|
||||||
}}
|
|
||||||
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')}
|
|
||||||
>
|
>
|
||||||
|
{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>
|
<div>
|
||||||
A schema change is considered as breaking only if it affects more than
|
A schema change is considered as breaking only if it affects more than
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -686,7 +691,7 @@ const ConditionalBreakingChanges = (props: {
|
||||||
{touched.targets && errors.targets && (
|
{touched.targets && errors.targets && (
|
||||||
<div className="text-red-500">{errors.targets}</div>
|
<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>
|
||||||
<div className="font-semibold">Example settings</div>
|
<div className="font-semibold">Example settings</div>
|
||||||
<div className="text-sm">Removal of a field is considered breaking if</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
|
- the field was requested by more than 10% of all GraphQL operations in recent 30 days
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -715,8 +718,8 @@ const ConditionalBreakingChanges = (props: {
|
||||||
{mutation.error.graphQLErrors[0]?.message ?? mutation.error.message}
|
{mutation.error.graphQLErrors[0]?.message ?? mutation.error.message}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -764,6 +767,9 @@ function TargetName(props: {
|
||||||
projectId: props.projectId,
|
projectId: props.projectId,
|
||||||
targetId: newTargetId,
|
targetId: newTargetId,
|
||||||
},
|
},
|
||||||
|
search: {
|
||||||
|
page: subPages[0].key,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else if (result.error || result.data?.updateTargetName.error?.message) {
|
} else if (result.error || result.data?.updateTargetName.error?.message) {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -776,53 +782,55 @@ function TargetName(props: {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle>Target Name</CardTitle>
|
title="Target Name"
|
||||||
<CardDescription>
|
description={
|
||||||
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>
|
Changing the name of your target will also change the slug of your target URL, and
|
||||||
<CardDescription>
|
will invalidate any existing links to your target.
|
||||||
<DocsLink
|
</CardDescription>
|
||||||
href="/management/targets#rename-a-target"
|
<CardDescription>
|
||||||
className="text-gray-500 hover:text-gray-300"
|
<DocsLink
|
||||||
>
|
href="/management/targets#rename-a-target"
|
||||||
You can read more about it in the documentation
|
className="text-gray-500 hover:text-gray-300"
|
||||||
</DocsLink>
|
>
|
||||||
</CardDescription>
|
You can read more about it in the documentation
|
||||||
</CardHeader>
|
</DocsLink>
|
||||||
|
</CardDescription>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<CardContent>
|
<div className="flex flex-row items-center gap-x-2">
|
||||||
<div className="flex flex-row items-center gap-x-2">
|
<Input
|
||||||
<Input
|
placeholder="Target name"
|
||||||
placeholder="Target name"
|
name="name"
|
||||||
name="name"
|
value={values.name}
|
||||||
value={values.name}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
onBlur={handleBlur}
|
||||||
onBlur={handleBlur}
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
isInvalid={touched.name && !!errors.name}
|
||||||
isInvalid={touched.name && !!errors.name}
|
className="w-96"
|
||||||
className="w-96"
|
/>
|
||||||
/>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
Save
|
||||||
Save
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{touched.name && (errors.name || mutation.error) && (
|
{touched.name && (errors.name || mutation.error) && (
|
||||||
<div className="mt-2 text-red-500">
|
<div className="mt-2 text-red-500">
|
||||||
{errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message}
|
{errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mutation.data?.updateTargetName.error?.inputErrors?.name && (
|
{mutation.data?.updateTargetName.error?.inputErrors?.name && (
|
||||||
<div className="mt-2 text-red-500">
|
<div className="mt-2 text-red-500">
|
||||||
{mutation.data.updateTargetName.error.inputErrors.name}
|
{mutation.data.updateTargetName.error.inputErrors.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -891,56 +899,58 @@ function GraphQLEndpointUrl(props: {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<SubPageLayout>
|
||||||
<CardHeader>
|
<SubPageLayoutHeader
|
||||||
<CardTitle>GraphQL Endpoint URL</CardTitle>
|
title="GraphQL Endpoint URL"
|
||||||
<CardDescription>
|
description={
|
||||||
The endpoint url will be used for querying the target from the{' '}
|
<>
|
||||||
<Link
|
<CardDescription>
|
||||||
to="/$organizationId/$projectId/$targetId/laboratory"
|
The endpoint url will be used for querying the target from the{' '}
|
||||||
params={{
|
<Link
|
||||||
organizationId: props.organizationId,
|
to="/$organizationId/$projectId/$targetId/laboratory"
|
||||||
projectId: props.projectId,
|
params={{
|
||||||
targetId: props.targetId,
|
organizationId: props.organizationId,
|
||||||
}}
|
projectId: props.projectId,
|
||||||
>
|
targetId: props.targetId,
|
||||||
Hive Laboratory
|
}}
|
||||||
</Link>
|
>
|
||||||
.
|
Hive Laboratory
|
||||||
</CardDescription>
|
</Link>
|
||||||
</CardHeader>
|
.
|
||||||
|
</CardDescription>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<CardContent>
|
<div className="flex flex-row items-center gap-x-2">
|
||||||
<div className="flex flex-row items-center gap-x-2">
|
<Input
|
||||||
<Input
|
placeholder="Endpoint Url"
|
||||||
placeholder="Endpoint Url"
|
name="graphqlEndpointUrl"
|
||||||
name="graphqlEndpointUrl"
|
value={values.graphqlEndpointUrl}
|
||||||
value={values.graphqlEndpointUrl}
|
onChange={handleChange}
|
||||||
onChange={handleChange}
|
onBlur={handleBlur}
|
||||||
onBlur={handleBlur}
|
disabled={isSubmitting}
|
||||||
disabled={isSubmitting}
|
isInvalid={touched.graphqlEndpointUrl && !!errors.graphqlEndpointUrl}
|
||||||
isInvalid={touched.graphqlEndpointUrl && !!errors.graphqlEndpointUrl}
|
className="w-96"
|
||||||
className="w-96"
|
/>
|
||||||
/>
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
Save
|
||||||
Save
|
</Button>
|
||||||
</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>
|
</div>
|
||||||
{touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && (
|
)}
|
||||||
<div className="mt-2 text-red-500">
|
{mutation.data?.updateTargetGraphQLEndpointUrl.error && (
|
||||||
{errors.graphqlEndpointUrl ??
|
<div className="mt-2 text-red-500">
|
||||||
mutation.error?.graphQLErrors[0]?.message ??
|
{mutation.data.updateTargetGraphQLEndpointUrl.error.message}
|
||||||
mutation.error?.message}
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
{mutation.data?.updateTargetGraphQLEndpointUrl.error && (
|
|
||||||
<div className="mt-2 text-red-500">
|
|
||||||
{mutation.data.updateTargetGraphQLEndpointUrl.error.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -991,29 +1001,30 @@ function TargetDelete(props: { organizationId: string; projectId: string; target
|
||||||
const [isModalOpen, toggleModalOpen] = useToggle();
|
const [isModalOpen, toggleModalOpen] = useToggle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SubPageLayout>
|
||||||
<Card className="mb-10">
|
<SubPageLayoutHeader
|
||||||
<CardHeader>
|
title="Delete Target"
|
||||||
<CardTitle>Delete Target</CardTitle>
|
description={
|
||||||
<CardDescription>
|
<>
|
||||||
Deleting an project also delete all schemas and data associated with it.
|
<CardDescription>
|
||||||
</CardDescription>
|
Deleting an project also delete all schemas and data associated with it.
|
||||||
<CardDescription>
|
</CardDescription>
|
||||||
<DocsLink
|
<CardDescription>
|
||||||
href="/management/targets#delete-a-target"
|
<DocsLink
|
||||||
className="text-gray-500 hover:text-gray-300"
|
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
|
<strong>This action is not reversible!</strong> You can find more information about
|
||||||
</DocsLink>
|
this process in the documentation
|
||||||
</CardDescription>
|
</DocsLink>
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardFooter>
|
</>
|
||||||
<Button variant="destructive" onClick={toggleModalOpen}>
|
}
|
||||||
Delete Target
|
/>
|
||||||
</Button>
|
<Button variant="destructive" onClick={toggleModalOpen}>
|
||||||
</CardFooter>
|
Delete Target
|
||||||
</Card>
|
</Button>
|
||||||
|
|
||||||
<DeleteTargetModal
|
<DeleteTargetModal
|
||||||
organizationId={props.organizationId}
|
organizationId={props.organizationId}
|
||||||
projectId={props.projectId}
|
projectId={props.projectId}
|
||||||
|
|
@ -1021,7 +1032,7 @@ function TargetDelete(props: { organizationId: string; projectId: string; target
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
toggleModalOpen={toggleModalOpen}
|
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: {
|
function TargetSettingsContent(props: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
page?: SubPage;
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
const [query] = useQuery({
|
const [query] = useQuery({
|
||||||
query: TargetSettingsPageQuery,
|
query: TargetSettingsPageQuery,
|
||||||
variables: {
|
variables: {
|
||||||
|
|
@ -1073,6 +1115,7 @@ function TargetSettingsContent(props: {
|
||||||
TargetSettingsPage_OrganizationFragment,
|
TargetSettingsPage_OrganizationFragment,
|
||||||
currentOrganization,
|
currentOrganization,
|
||||||
);
|
);
|
||||||
|
|
||||||
const targetForSettings = useFragment(TargetSettingsPage_TargetFragment, currentTarget);
|
const targetForSettings = useFragment(TargetSettingsPage_TargetFragment, currentTarget);
|
||||||
|
|
||||||
const canAccessTokens = canAccessTarget(
|
const canAccessTokens = canAccessTarget(
|
||||||
|
|
@ -1092,66 +1135,103 @@ function TargetSettingsContent(props: {
|
||||||
organizationId={props.organizationId}
|
organizationId={props.organizationId}
|
||||||
page={Page.Settings}
|
page={Page.Settings}
|
||||||
>
|
>
|
||||||
<div className="py-6">
|
|
||||||
<Title>Settings</Title>
|
|
||||||
<Subtitle>Manage your target settings.</Subtitle>
|
|
||||||
</div>
|
|
||||||
{currentOrganization && currentProject && currentTarget && organizationForSettings ? (
|
{currentOrganization && currentProject && currentTarget && organizationForSettings ? (
|
||||||
<div className="flex flex-col gap-y-4">
|
<PageLayout>
|
||||||
<TargetName
|
<NavLayout>
|
||||||
targetName={currentTarget.name}
|
{subPages.map(subPage => {
|
||||||
targetId={currentTarget.cleanId}
|
if (
|
||||||
projectId={currentProject.cleanId}
|
subPage.key === 'schema-contracts' &&
|
||||||
organizationId={currentOrganization.cleanId}
|
currentProject.type !== ProjectType.Federation
|
||||||
/>
|
) {
|
||||||
<GraphQLEndpointUrl
|
return null;
|
||||||
targetId={currentTarget.cleanId}
|
}
|
||||||
projectId={currentProject.cleanId}
|
return (
|
||||||
organizationId={currentOrganization.cleanId}
|
<Button
|
||||||
graphqlEndpointUrl={currentTarget.graphqlEndpointUrl ?? null}
|
key={subPage.key}
|
||||||
/>
|
variant="ghost"
|
||||||
{canAccessTokens && (
|
onClick={() => {
|
||||||
<RegistryAccessTokens
|
void router.navigate({
|
||||||
targetId={currentTarget.cleanId}
|
search: {
|
||||||
projectId={currentProject.cleanId}
|
page: subPage.key,
|
||||||
organizationId={currentOrganization.cleanId}
|
},
|
||||||
me={organizationForSettings.me}
|
});
|
||||||
/>
|
}}
|
||||||
)}
|
className={cn(
|
||||||
{canAccessTokens && (
|
props.page === subPage.key
|
||||||
<CDNAccessTokens
|
? 'bg-muted hover:bg-muted'
|
||||||
organizationId={props.organizationId}
|
: 'hover:bg-transparent hover:underline',
|
||||||
projectId={props.projectId}
|
'w-full justify-start text-left',
|
||||||
targetId={props.targetId}
|
)}
|
||||||
me={organizationForSettings.me}
|
>
|
||||||
/>
|
{subPage.title}
|
||||||
)}
|
</Button>
|
||||||
{currentProject.type === ProjectType.Federation && (
|
);
|
||||||
<SchemaContracts
|
})}
|
||||||
organizationId={props.organizationId}
|
</NavLayout>
|
||||||
projectId={props.projectId}
|
<PageLayoutContent>
|
||||||
targetId={props.targetId}
|
{currentOrganization && currentProject && currentTarget && organizationForSettings ? (
|
||||||
/>
|
<div className="space-y-12">
|
||||||
)}
|
{props.page === 'general' ? (
|
||||||
<ConditionalBreakingChanges
|
<>
|
||||||
targetId={currentTarget.cleanId}
|
<TargetName
|
||||||
projectId={currentProject.cleanId}
|
targetName={currentTarget.name}
|
||||||
organizationId={currentOrganization.cleanId}
|
targetId={currentTarget.cleanId}
|
||||||
/>
|
projectId={currentProject.cleanId}
|
||||||
<ExtendBaseSchema
|
organizationId={currentOrganization.cleanId}
|
||||||
targetId={currentTarget.cleanId}
|
/>
|
||||||
projectId={currentProject.cleanId}
|
<GraphQLEndpointUrl
|
||||||
organizationId={currentOrganization.cleanId}
|
targetId={currentTarget.cleanId}
|
||||||
baseSchema={targetForSettings?.baseSchema ?? ''}
|
projectId={currentProject.cleanId}
|
||||||
/>
|
organizationId={currentOrganization.cleanId}
|
||||||
{canDelete && (
|
graphqlEndpointUrl={currentTarget.graphqlEndpointUrl ?? null}
|
||||||
<TargetDelete
|
/>
|
||||||
targetId={currentTarget.cleanId}
|
{canDelete && (
|
||||||
projectId={currentProject.cleanId}
|
<TargetDelete
|
||||||
organizationId={currentOrganization.cleanId}
|
targetId={currentTarget.cleanId}
|
||||||
/>
|
projectId={currentProject.cleanId}
|
||||||
)}
|
organizationId={currentOrganization.cleanId}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : 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}
|
) : null}
|
||||||
</TargetLayout>
|
</TargetLayout>
|
||||||
);
|
);
|
||||||
|
|
@ -1161,11 +1241,17 @@ export function TargetSettingsPage(props: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
|
page?: SubPage;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Settings" />
|
<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({
|
const targetSettingsRoute = createRoute({
|
||||||
getParentRoute: () => targetRoute,
|
getParentRoute: () => targetRoute,
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
|
validateSearch(search) {
|
||||||
|
return TargetSettingRouteSearch.parse(search);
|
||||||
|
},
|
||||||
component: function TargetSettingsRoute() {
|
component: function TargetSettingsRoute() {
|
||||||
const { organizationId, projectId, targetId } = targetSettingsRoute.useParams();
|
const { organizationId, projectId, targetId } = targetSettingsRoute.useParams();
|
||||||
|
const { page } = targetSettingsRoute.useSearch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TargetSettingsPage
|
<TargetSettingsPage
|
||||||
organizationId={organizationId}
|
organizationId={organizationId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
targetId={targetId}
|
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