diff --git a/packages/web/app/.storybook/preview.js b/packages/web/app/.storybook/preview.js index 3adc73b70..a9553b00e 100644 --- a/packages/web/app/.storybook/preview.js +++ b/packages/web/app/.storybook/preview.js @@ -8,7 +8,7 @@ if (!document.body.className.includes('dark')) { const preview = { parameters: { backgrounds: { - default: 'dark', + default: 'black', }, actions: { argTypesRegex: '^on[A-Z].*' }, controls: { diff --git a/packages/web/app/src/components/layouts/target.tsx b/packages/web/app/src/components/layouts/target.tsx index f8d7904fe..45768e178 100644 --- a/packages/web/app/src/components/layouts/target.tsx +++ b/packages/web/app/src/components/layouts/target.tsx @@ -233,6 +233,7 @@ export const TargetLayout = ({ projectId: props.projectId, targetId: props.targetId, }} + search={{ page: 'general' }} > Settings diff --git a/packages/web/app/src/components/organization/members/invitations.tsx b/packages/web/app/src/components/organization/members/invitations.tsx index 64d7e4a57..92ceed0be 100644 --- a/packages/web/app/src/components/organization/members/invitations.tsx +++ b/packages/web/app/src/components/organization/members/invitations.tsx @@ -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 ( -
-
-
- Pending invitations - - Active invitations to join this organization. Invitations expire after 7 days. - -
-
- -
-
+ + + + {organization.invitations.nodes.length > 0 ? ( @@ -476,6 +471,6 @@ export function OrganizationInvitations(props: { )} - + ); } diff --git a/packages/web/app/src/components/organization/members/list.tsx b/packages/web/app/src/components/organization/members/list.tsx index 36edf525d..bb1b07c83 100644 --- a/packages/web/app/src/components/organization/members/list.tsx +++ b/packages/web/app/src/components/organization/members/list.tsx @@ -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 ( -
-
-
- List of organization members - - Manage the members of your organization and their permissions. - -
-
- -
-
+ + + +
@@ -589,6 +584,6 @@ export function OrganizationMembers(props: { ))}
-
+ ); } diff --git a/packages/web/app/src/components/organization/members/migration.tsx b/packages/web/app/src/components/organization/members/migration.tsx index bd8f324a6..d41d67e3b 100644 --- a/packages/web/app/src/components/organization/members/migration.tsx +++ b/packages/web/app/src/components/organization/members/migration.tsx @@ -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 ( -
-
- Migration Wizard - - This wizard will help you migrate your organization's members to the new permissions - system. - - - Members are grouped by their access scopes. -
You can choose to migrate all members from each group to a new role or assign them - to an existing role. -
- - Read "Introducing Member Roles" product update to learn more. - -
+ + + + This wizard will help you migrate your organization's members to the new permissions + system. + + + Members are grouped by their access scopes. +
You can choose to migrate all members from each group to a new role or assign + them to an existing role. +
+ + Read "Introducing Member Roles" product update to learn more. + + + } + /> {organization.unassignedMembersToMigrate.length > 0 ? ( @@ -900,6 +905,6 @@ export function OrganizationMemberRolesMigration(props: { )} - + ); } diff --git a/packages/web/app/src/components/organization/members/roles.tsx b/packages/web/app/src/components/organization/members/roles.tsx index 903375d92..5b2b94534 100644 --- a/packages/web/app/src/components/organization/members/roles.tsx +++ b/packages/web/app/src/components/organization/members/roles.tsx @@ -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: { ) : null} -
-
-
- List of roles - - Manage the roles that can be assigned to members of this organization. - -
-
- -
-
+ + + +
@@ -930,7 +925,7 @@ export function OrganizationMemberRoles(props: { ))}
-
+ ); } diff --git a/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx b/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx index a624c1d23..288b278c5 100644 --- a/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx +++ b/packages/web/app/src/components/target/settings/cdn-access-tokens.tsx @@ -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 ( - - - CDN Access Token - - CDN Access Tokens are used to access to Hive High-Availability CDN and read your schema - artifacts. - - - - Learn more about CDN Access Tokens - - - - - {canManage && ( -
- -
- )} - - - {target?.data?.target?.cdnAccessTokens.edges?.map(edge => { - const node = useFragment(CDNAccessTokeRowFragment, edge.node); - - return ( - - - - - - - ); - })} - -
- {node.firstCharacters + new Array(10).fill('•').join('') + node.lastCharacters} - {node.alias} - created - - -
- -
- {target.data?.target?.cdnAccessTokens.pageInfo.hasPreviousPage ? ( - - ) : null} - {target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage ? ( - - ) : null} + Create new CDN token + +
-
+ )} + + + {target?.data?.target?.cdnAccessTokens.edges?.map(edge => { + const node = useFragment(CDNAccessTokeRowFragment, edge.node); + + return ( + + + + + + + ); + })} + +
+ {node.firstCharacters + new Array(10).fill('•').join('') + node.lastCharacters} + {node.alias} + created + + +
+ +
+ {target.data?.target?.cdnAccessTokens.pageInfo.hasPreviousPage ? ( + + ) : null} + {target.data?.target?.cdnAccessTokens.pageInfo.hasNextPage ? ( + + ) : null} +
+ {searchParams.cdn === 'create' ? ( { @@ -499,6 +507,6 @@ export function CDNAccessTokens(props: { targetId={props.targetId} /> ) : null} -
+ ); } diff --git a/packages/web/app/src/components/target/settings/schema-contracts.tsx b/packages/web/app/src/components/target/settings/schema-contracts.tsx index dcab24cf8..adde3c991 100644 --- a/packages/web/app/src/components/target/settings/schema-contracts.tsx +++ b/packages/web/app/src/components/target/settings/schema-contracts.tsx @@ -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 ( <> - - - Schema Contracts - - Schema Contracts allow you to have separate public graphs that are a subset of the main - graph. - - - - Learn more about Schema Contracts - - - - -
- - - - - - - - -
- {!!contracts?.length && ( - - - - Contract Name - Status - Included Tags - Excluded Tags - Remove unreachable API Types - Created at - - - - - {contracts.map(({ node }) => ( - - - {node.contractName} - - -
- {node.isDisabled ? ( - <> - Inactive - - - - - - -

- This Contract is no longer active and no more contract versions - or contract checks will be published for it. -

-

- It is not possible to enable a contract again. Please create a - new contract instead. -

-
-
-
- - ) : ( - <> - Active - - - - - - -

- This Contract is active. Schema publishes and checks will - attempt to also build the contract schema. -

-
-
-
- - )} -
-
- - {node.includeTags?.map(tag => ( - - {tag} - - )) ?? 'None'} - - - {node.excludeTags?.map(tag => ( - - {tag} - - )) ?? 'None'} - - - {node.removeUnreachableTypesFromPublicApiSchema ? ( - - ) : ( - - )} - - - - - - {node.viewerCanDisableContract && ( - - - - - - Actions - onDisable(node.id)} - > - Disable - - - - )} - -
- ))} -
-
- )} -
-
- {disabledContractId && ( - { - setDisabledContractId(null); - }} + + + + Schema Contracts allow you to have separate public graphs that are a subset of the + main graph. + + + + Learn more about Schema Contracts + + + + } /> - )} +
+ + + + + + + + +
+ {!!contracts?.length && ( + + + + Contract Name + Status + Included Tags + Excluded Tags + Remove unreachable API Types + Created at + + + + + {contracts.map(({ node }) => ( + + + {node.contractName} + + +
+ {node.isDisabled ? ( + <> + Inactive + + + + + + +

+ This Contract is no longer active and no more contract versions or + contract checks will be published for it. +

+

+ It is not possible to enable a contract again. Please create a new + contract instead. +

+
+
+
+ + ) : ( + <> + Active + + + + + + +

+ This Contract is active. Schema publishes and checks will attempt + to also build the contract schema. +

+
+
+
+ + )} +
+
+ + {node.includeTags?.map(tag => ( + + {tag} + + )) ?? 'None'} + + + {node.excludeTags?.map(tag => ( + + {tag} + + )) ?? 'None'} + + + {node.removeUnreachableTypesFromPublicApiSchema ? ( + + ) : ( + + )} + + + + + + {node.viewerCanDisableContract && ( + + + + + + Actions + onDisable(node.id)} + > + Disable + + + + )} + +
+ ))} +
+
+ )} + {disabledContractId && ( + { + setDisabledContractId(null); + }} + /> + )} +
); } diff --git a/packages/web/app/src/components/ui/page-content-layout.tsx b/packages/web/app/src/components/ui/page-content-layout.tsx new file mode 100644 index 000000000..d91c9359e --- /dev/null +++ b/packages/web/app/src/components/ui/page-content-layout.tsx @@ -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; + +const NavLayout = forwardRef(({ children, ...props }, ref) => ( + +)); +NavLayout.displayName = 'NavLayout'; + +type PageLayoutProps = { + children: ReactNode; +} & HTMLAttributes; + +const PageLayout = forwardRef(({ children, ...props }, ref) => ( +
+
+ {children} +
+
+)); +PageLayout.displayName = 'PageLayout'; + +type PageLayoutContentProps = { + children: ReactNode; + mainTitlePage?: string; +} & HTMLAttributes; + +const PageLayoutContent = forwardRef( + ({ children, mainTitlePage, ...props }, ref) => ( +
+

{mainTitlePage}

+ {mainTitlePage ?
: null} + {children} +
+ ), +); +PageLayoutContent.displayName = 'PageLayoutContent'; + +const SubPageLayout = forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +SubPageLayout.displayName = 'SubPageLayout'; + +type SubPageLayoutHeaderProps = { + children?: ReactNode; + title?: string; + description?: string | ReactNode; +} & HTMLAttributes; + +const SubPageLayoutHeader = forwardRef(({ ...props }) => ( +
+
+ {props.title} + {typeof props.description === 'string' ? ( + {props.description} + ) : ( + props.description + )} +
+
{props.children}
+
+)); +SubPageLayoutHeader.displayName = 'SubPageLayoutHeader'; + +export { PageLayout, NavLayout, PageLayoutContent, SubPageLayout, SubPageLayoutHeader }; diff --git a/packages/web/app/src/components/v2/modals/connect-schema.tsx b/packages/web/app/src/components/v2/modals/connect-schema.tsx index 9d758c450..f76a4b7a2 100644 --- a/packages/web/app/src/components/v2/modals/connect-schema.tsx +++ b/packages/web/app/src/components/v2/modals/connect-schema.tsx @@ -158,6 +158,9 @@ export const ConnectSchemaModal = (props: { To authenticate,{' '} -
- -
- {props.page === 'roles' ? : null} - {props.page === 'list' ? ( - - ) : null} - {props.page === 'invitations' ? ( - - ) : null} - {props.page === 'migration' && organization.me.isAdmin ? ( - - ) : null} -
-
-
+ + + {subPages.map(subPage => { + // hide migration page from non-admins + if (subPage.key === 'migration' && !organization.me.isAdmin) { + return null; + } + return ( + + ); + })} + + + {props.page === 'roles' ? : null} + {props.page === 'list' ? ( + + ) : null} + {props.page === 'invitations' ? ( + + ) : null} + {props.page === 'migration' && organization.me.isAdmin ? ( + + ) : null} + + ); } diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index d0c0cbf89..b2b7a2c44 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -965,6 +965,9 @@ function LaboratoryPageContent(props: { projectId: props.projectId, targetId: props.targetId, }} + search={{ + page: 'general', + }} > - {checked.length === 0 ? null : ( - - )} -
- )} - - - {tokens?.map(token => ( - - - - - - - - ))} - -
- - setChecked( - isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id), - ) - } - checked={checked.includes(token.id)} - disabled={!canManage} - /> - {token.alias}{token.name} - {token.lastUsedAt ? ( - <> - last used - - ) : ( - 'not used yet' - )} - - created -
- + + + + 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. + + + + Learn more about Registry Access Tokens + + + + } + /> + {canManage && ( +
+ + {checked.length === 0 ? null : ( + + )} +
+ )} + + + {tokens?.map(token => ( + + + + + + + + ))} + +
+ + setChecked( + isChecked ? [...checked, token.id] : checked.filter(k => k !== token.id), + ) + } + checked={checked.includes(token.id)} + disabled={!canManage} + /> + {token.alias}{token.name} + {token.lastUsedAt ? ( + <> + last used + + ) : ( + 'not used yet' + )} + + created +
{isModalOpen && ( )} - +
); } @@ -240,40 +242,42 @@ const ExtendBaseSchema = (props: { const isUnsaved = baseSchema?.trim() !== props.baseSchema?.trim(); return ( - - - Extend Your Schema - - Schema Extensions is pre-defined GraphQL schema that is automatically merged with your - published schemas, before being checked and validated. - - - - You can find more details and examples in the documentation - - - - - setBaseSchema(value ?? '')} - /> - {mutation.data?.updateBaseSchema.error && ( -
{mutation.data.updateBaseSchema.error.message}
- )} - {mutation.error && ( -
- {mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message} -
- )} -
- + + + + Schema Extensions is pre-defined GraphQL schema that is automatically merged with your + published schemas, before being checked and validated. + + + + You can find more details and examples in the documentation + + + + } + /> + setBaseSchema(value ?? '')} + /> + {mutation.data?.updateBaseSchema.error && ( +
{mutation.data.updateBaseSchema.error.message}
+ )} + {mutation.error && ( +
+ {mutation.error?.graphQLErrors[0]?.message ?? mutation.error.message} +
+ )} +
+
); }; @@ -532,46 +536,47 @@ const ConditionalBreakingChanges = (props: { return (
- - - -
Conditional Breaking Changes
- {targetSettings.fetching ? ( - - ) : ( - { - await setValidation({ - input: { - target: props.targetId, - project: props.projectId, - organization: props.organizationId, - enabled, - }, - }); - }} - disabled={targetValidation.fetching} - /> - )} -
- - Conditional Breaking Changes can change the behavior of schema checks, based on real - traffic data sent to Hive. - - - - Learn more - - -
- + + + Conditional Breaking Changes can change the behavior of schema checks, based on real + traffic data sent to Hive. + + + + Learn more + + + + } > + {targetSettings.fetching ? ( + + ) : ( + { + await setValidation({ + input: { + target: props.targetId, // targetId is the target we are updating + project: props.projectId, + organization: props.organizationId, + enabled, + }, + }); + }} + disabled={targetValidation.fetching} + /> + )} + +
A schema change is considered as breaking only if it affects more than {errors.targets}
)} -
+
Example settings
Removal of a field is considered breaking if
@@ -705,8 +710,6 @@ const ConditionalBreakingChanges = (props: { - the field was requested by more than 10% of all GraphQL operations in recent 30 days
- - @@ -715,8 +718,8 @@ const ConditionalBreakingChanges = (props: { {mutation.error.graphQLErrors[0]?.message ?? mutation.error.message} )} - - +
+ ); }; @@ -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 ( - - - Target Name - - Changing the name of your target will also change the slug of your target URL, and will - invalidate any existing links to your target. - - - - You can read more about it in the documentation - - - + + + + Changing the name of your target will also change the slug of your target URL, and + will invalidate any existing links to your target. + + + + You can read more about it in the documentation + + + + } + />
- -
- - -
+
+ + +
- {touched.name && (errors.name || mutation.error) && ( -
- {errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message} -
- )} - {mutation.data?.updateTargetName.error?.inputErrors?.name && ( -
- {mutation.data.updateTargetName.error.inputErrors.name} -
- )} -
+ {touched.name && (errors.name || mutation.error) && ( +
+ {errors.name ?? mutation.error?.graphQLErrors[0]?.message ?? mutation.error?.message} +
+ )} + {mutation.data?.updateTargetName.error?.inputErrors?.name && ( +
+ {mutation.data.updateTargetName.error.inputErrors.name} +
+ )}
-
+ ); } @@ -891,56 +899,58 @@ function GraphQLEndpointUrl(props: { }); return ( - - - GraphQL Endpoint URL - - The endpoint url will be used for querying the target from the{' '} - - Hive Laboratory - - . - - + + + + The endpoint url will be used for querying the target from the{' '} + + Hive Laboratory + + . + + + } + />
- -
- - +
+ + +
+ {touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && ( +
+ {errors.graphqlEndpointUrl ?? + mutation.error?.graphQLErrors[0]?.message ?? + mutation.error?.message}
- {touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && ( -
- {errors.graphqlEndpointUrl ?? - mutation.error?.graphQLErrors[0]?.message ?? - mutation.error?.message} -
- )} - {mutation.data?.updateTargetGraphQLEndpointUrl.error && ( -
- {mutation.data.updateTargetGraphQLEndpointUrl.error.message} -
- )} - + )} + {mutation.data?.updateTargetGraphQLEndpointUrl.error && ( +
+ {mutation.data.updateTargetGraphQLEndpointUrl.error.message} +
+ )} - + ); } @@ -991,29 +1001,30 @@ function TargetDelete(props: { organizationId: string; projectId: string; target const [isModalOpen, toggleModalOpen] = useToggle(); return ( - <> - - - Delete Target - - Deleting an project also delete all schemas and data associated with it. - - - - This action is not reversible! You can find more information about - this process in the documentation - - - - - - - + + + + Deleting an project also delete all schemas and data associated with it. + + + + This action is not reversible! You can find more information about + this process in the documentation + + + + } + /> + + - + ); } @@ -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} > -
- Settings - Manage your target settings. -
{currentOrganization && currentProject && currentTarget && organizationForSettings ? ( -
- - - {canAccessTokens && ( - - )} - {canAccessTokens && ( - - )} - {currentProject.type === ProjectType.Federation && ( - - )} - - - {canDelete && ( - - )} -
+ + + {subPages.map(subPage => { + if ( + subPage.key === 'schema-contracts' && + currentProject.type !== ProjectType.Federation + ) { + return null; + } + return ( + + ); + })} + + + {currentOrganization && currentProject && currentTarget && organizationForSettings ? ( +
+ {props.page === 'general' ? ( + <> + + + {canDelete && ( + + )} + + ) : null} + {props.page === 'cdn' && canAccessTokens ? ( + + ) : null} + {props.page === 'registry-token' && canAccessTokens ? ( + + ) : null} + {props.page === 'breaking-changes' ? ( + + ) : null} + {props.page === 'base-schema' ? ( + + ) : null} + {props.page === 'schema-contracts' ? ( + + ) : null} +
+ ) : null} +
+
) : null} ); @@ -1161,11 +1241,17 @@ export function TargetSettingsPage(props: { organizationId: string; projectId: string; targetId: string; + page?: SubPage; }) { return ( <> - + ); } diff --git a/packages/web/app/src/router.tsx b/packages/web/app/src/router.tsx index 58c1adebb..89e03d362 100644 --- a/packages/web/app/src/router.tsx +++ b/packages/web/app/src/router.tsx @@ -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 ( ); }, diff --git a/packages/web/app/src/stories/sub-page-layout.stories.tsx b/packages/web/app/src/stories/sub-page-layout.stories.tsx new file mode 100644 index 000000000..9c5581ccd --- /dev/null +++ b/packages/web/app/src/stories/sub-page-layout.stories.tsx @@ -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 = { + 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 = { + render: () => { + const [page, setPage] = useState('general'); + + return ( + + + {subPages.map(subPage => { + return ( + + ); + })} + + subPage.key === page)?.title}> + {page === 'general' &&
General
} + {page === 'cdn' &&
CDN Tokens
} + {page === 'registry-token' &&
Registry Tokens
} + {page === 'breaking-changes' &&
Breaking Changes
} + {page === 'base-schema' &&
Base Schema
} + {page === 'schema-contracts' &&
Schema Contracts
} +
+
+ ); + }, +}; + +export const Default = Template;