diff --git a/packages/web/app/src/components/layouts/organization.tsx b/packages/web/app/src/components/layouts/organization.tsx index 28307f5510..d8922d47c2 100644 --- a/packages/web/app/src/components/layouts/organization.tsx +++ b/packages/web/app/src/components/layouts/organization.tsx @@ -45,7 +45,6 @@ export enum Page { Overview = 'overview', Members = 'members', Settings = 'settings', - Policy = 'policy', Support = 'support', Subscription = 'subscription', } @@ -54,11 +53,9 @@ const OrganizationLayout_OrganizationFragment = graphql(` fragment OrganizationLayout_OrganizationFragment on Organization { id slug - viewerCanModifySchemaPolicy viewerCanCreateProject viewerCanManageSupportTickets viewerCanDescribeBilling - viewerCanAccessSettings viewerCanSeeMembers ...ProPlanBilling_OrganizationFragment ...RateLimitWarn_OrganizationFragment @@ -156,24 +153,14 @@ export function OrganizationLayout({ )} - + - Policy + Settings - {currentOrganization.viewerCanAccessSettings && ( - - - Settings - - - )} {currentOrganization.viewerCanManageSupportTickets && ( )} - + - Policy + Settings - {currentProject.viewerCanModifySettings && ( - - - Settings - - - )} ) : ( diff --git a/packages/web/app/src/components/project/settings/composition.tsx b/packages/web/app/src/components/project/settings/composition.tsx index ad1633a393..13946b0de8 100644 --- a/packages/web/app/src/components/project/settings/composition.tsx +++ b/packages/web/app/src/components/project/settings/composition.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; import { useMutation, useQuery } from 'urql'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { CardDescription } from '@/components/ui/card'; import { CheckIcon } from '@/components/ui/icon'; +import { SubPageLayout, SubPageLayoutHeader } from '@/components/ui/page-content-layout'; import { Spinner } from '@/components/ui/spinner'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FragmentType, graphql, useFragment } from '@/gql'; @@ -90,14 +91,12 @@ export const CompositionSettings = (props: { }; return ( - - - - Schema Composition - - Configure how your schemas are composed. - - + + Schema Composition} + description={Configure how your schemas are composed.} + /> +
{projectQuery.fetching ? ( ) : ( @@ -149,7 +148,7 @@ export const CompositionSettings = (props: { )} - - +
+
); }; diff --git a/packages/web/app/src/pages/organization-policy.tsx b/packages/web/app/src/pages/organization-policy.tsx deleted file mode 100644 index 0587eaf3a9..0000000000 --- a/packages/web/app/src/pages/organization-policy.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { ReactElement } from 'react'; -import { useMutation, useQuery } from 'urql'; -import { OrganizationLayout, Page } from '@/components/layouts/organization'; -import { PolicySettings } from '@/components/policy/policy-settings'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Checkbox } from '@/components/ui/checkbox'; -import { DocsLink } from '@/components/ui/docs-note'; -import { Meta } from '@/components/ui/meta'; -import { Subtitle, Title } from '@/components/ui/page'; -import { QueryError } from '@/components/ui/query-error'; -import { useToast } from '@/components/ui/use-toast'; -import { graphql } from '@/gql'; - -const OrganizationPolicyPageQuery = graphql(` - query OrganizationPolicyPageQuery($organizationSlug: String!) { - organization: organizationBySlug(organizationSlug: $organizationSlug) { - id - schemaPolicy { - id - updatedAt - ...PolicySettings_SchemaPolicyFragment - } - viewerCanModifySchemaPolicy - } - } -`); - -const UpdateSchemaPolicyForOrganization = graphql(` - mutation UpdateSchemaPolicyForOrganization( - $selector: OrganizationSelectorInput! - $policy: SchemaPolicyInput! - $allowOverrides: Boolean! - ) { - updateSchemaPolicyForOrganization( - selector: $selector - policy: $policy - allowOverrides: $allowOverrides - ) { - error { - message - } - ok { - organization { - id - schemaPolicy { - id - updatedAt - allowOverrides - ...PolicySettings_SchemaPolicyFragment - } - } - } - } - } -`); - -function PolicyPageContent(props: { organizationSlug: string }) { - const [query] = useQuery({ - query: OrganizationPolicyPageQuery, - variables: { - organizationSlug: props.organizationSlug, - }, - }); - const [mutation, mutate] = useMutation(UpdateSchemaPolicyForOrganization); - const { toast } = useToast(); - - const currentOrganization = query.data?.organization; - - if (query.error) { - return ; - } - - return ( - -
-
- Organization Schema Policy - - Schema Policies enable developers to define additional semantic checks on the GraphQL - schema. - -
- {currentOrganization ? ( - - - Rules - - At the organizational level, policies can be defined to affect all projects and - targets. -
- At the project level, policies can be overridden or extended. -
- - Learn more - -
-
- - { - await mutate({ - selector: { - organizationSlug: props.organizationSlug, - }, - policy: newPolicy, - allowOverrides, - }) - .then(result => { - if ( - result.data?.updateSchemaPolicyForOrganization.error || - result.error - ) { - toast({ - variant: 'destructive', - title: 'Error', - description: - result.data?.updateSchemaPolicyForOrganization.error?.message || - result.error?.message, - }); - } else { - toast({ - variant: 'default', - title: 'Success', - description: 'Policy updated successfully', - }); - } - }) - .catch(); - } - : null - } - currentState={currentOrganization.schemaPolicy} - > - {form => ( -
- form.setFieldValue('allowOverrides', newValue)} - disabled={!currentOrganization.viewerCanModifySchemaPolicy} - /> - -
- )} -
-
-
- ) : null} -
-
- ); -} - -export function OrganizationPolicyPage(props: { organizationSlug: string }): ReactElement { - return ( - <> - - - - ); -} diff --git a/packages/web/app/src/pages/organization-settings.tsx b/packages/web/app/src/pages/organization-settings.tsx index 843c0c4b88..c9a272eafa 100644 --- a/packages/web/app/src/pages/organization-settings.tsx +++ b/packages/web/app/src/pages/organization-settings.tsx @@ -6,15 +6,10 @@ import { z } from 'zod'; import { OrganizationLayout, Page } from '@/components/layouts/organization'; import { AccessTokensSubPage } from '@/components/organization/settings/access-tokens/access-tokens-sub-page'; import { OIDCIntegrationSection } from '@/components/organization/settings/oidc-integration-section'; +import { PolicySettings } from '@/components/policy/policy-settings'; import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { CardDescription } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, @@ -28,7 +23,13 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component import { GitHubIcon, SlackIcon } from '@/components/ui/icon'; import { Input } from '@/components/ui/input'; import { Meta } from '@/components/ui/meta'; -import { NavLayout, PageLayout, PageLayoutContent } from '@/components/ui/page-content-layout'; +import { + NavLayout, + PageLayout, + PageLayoutContent, + SubPageLayout, + SubPageLayoutHeader, +} from '@/components/ui/page-content-layout'; import { QueryError } from '@/components/ui/query-error'; import { ResourceDetails } from '@/components/ui/resource-details'; import { useToast } from '@/components/ui/use-toast'; @@ -201,7 +202,7 @@ const SlugFormSchema = z.object({ type SlugFormValues = z.infer; -const SettingsPageRenderer = (props: { +const OrganizationSettingsContent = (props: { organization: FragmentType; organizationSlug: string; }) => { @@ -263,224 +264,366 @@ const SettingsPageRenderer = (props: { ); return ( -
+
{organization.viewerCanModifySlug && (
- - - Organization Slug - - This is your organization's URL namespace on GraphQL Hive. Changing it{' '} - will invalidate any existing links to your - organization. -
- - You can read more about it in the documentation - -
-
- - ( - - -
-
- {env.appBaseUrl.replace(/https?:\/\//i, '')}/ -
- + + + + This is your organization's URL namespace on GraphQL Hive. Changing it{' '} + will invalidate any existing links to your + organization. + + + + You can read more about it in the documentation + + + + } + /> + ( + + +
+
+ {env.appBaseUrl.replace(/https?:\/\//i, '')}/
- - - - )} - /> - - - - - + +
+
+ +
+ )} + /> + +
)} {organization.viewerCanManageOIDCIntegration && ( - - - Single Sign On Provider - - Link your Hive organization to a single-sign-on provider such as Okta or Microsoft - Entra ID via OpenID Connect. -
- - Instructions for connecting your provider. - -
-
- -
- -
-
-
+ + + + Link your Hive organization to a single-sign-on provider such as Okta or Microsoft + Entra ID via OpenID Connect. + + + + Instructions for connecting your provider. + + + + } + /> +
+ +
+
)} {organization.viewerCanModifySlackIntegration && ( - - - Slack Integration - - Link your Hive organization with Slack for schema change notifications. -
- - Learn more. - -
-
- - - -
+ + + + Link your Hive organization with Slack for schema change notifications. + + + + Learn more. + + + + } + /> + + )} {organization.viewerCanModifyGitHubIntegration && ( - - - GitHub Integration - - Link your Hive organization with GitHub. -
- - Learn more. - -
-
- - - -
+ + + Link your Hive organization with GitHub. + + + Learn more. + + + + } + /> + + )} {organization.viewerCanTransferOwnership && ( - - - Transfer Ownership - - You are currently the owner of the organization. You can transfer the - organization to another member of the organization, or to an external user. -
- - Learn more about the process - -
-
- -
-
- - -
-
-
-
+ + + + You are currently the owner of the organization. You can transfer + the organization to another member of the organization, or to an external user. + + + + Learn more about the process + + + + } + /> + + + )} {organization.viewerCanDelete && ( - - - Delete Organization - - Deleting an organization will delete all the projects, targets, schemas and data - associated with it. -
- - This action is not reversible! You can find more information about - this process in the documentation - -
-
- - - - -
+ + + + Deleting an organization will delete all the projects, targets, schemas and data + associated with it. + + + + + This action is not reversible! You can find more information + about this process in the documentation + + + + + } + /> + + + )} {organization.viewerCanExportAuditLogs && ( - - - Audit Logs - - View a history of changes made to the organization settings. - - - -
-
- - -
-
-
-
+ + + + View a history of changes made to the organization settings. + + + + Learn more. + + + + } + /> + + + )}
); }; +const OrganizationPolicySettings_OrganizationFragment = graphql(` + fragment OrganizationPolicySettings_OrganizationFragment on Organization { + id + slug + schemaPolicy { + id + updatedAt + ...PolicySettings_SchemaPolicyFragment + } + viewerCanModifySchemaPolicy + } +`); + +const UpdateSchemaPolicyForOrganization = graphql(` + mutation UpdateSchemaPolicyForOrganization( + $selector: OrganizationSelectorInput! + $policy: SchemaPolicyInput! + $allowOverrides: Boolean! + ) { + updateSchemaPolicyForOrganization( + selector: $selector + policy: $policy + allowOverrides: $allowOverrides + ) { + error { + message + } + ok { + organization { + id + schemaPolicy { + id + updatedAt + allowOverrides + ...PolicySettings_SchemaPolicyFragment + } + } + } + } + } +`); + +function OrganizationPolicySettings(props: { + organization: FragmentType; +}) { + const [mutation, mutate] = useMutation(UpdateSchemaPolicyForOrganization); + const { toast } = useToast(); + + const currentOrganization = useFragment( + OrganizationPolicySettings_OrganizationFragment, + props.organization, + ); + + return ( + + + At the organizational level, policies can be defined to affect all projects and targets. +
+ At the project level, policies can be overridden or extended. +
+ + Learn more + + + } + /> + { + await mutate({ + selector: { + organizationSlug: currentOrganization.slug, + }, + policy: newPolicy, + allowOverrides, + }) + .then(result => { + if (result.data?.updateSchemaPolicyForOrganization.error || result.error) { + toast({ + variant: 'destructive', + title: 'Error', + description: + result.data?.updateSchemaPolicyForOrganization.error?.message || + result.error?.message, + }); + } else { + toast({ + variant: 'default', + title: 'Success', + description: 'Policy updated successfully', + }); + } + }) + .catch(); + } + : null + } + currentState={currentOrganization.schemaPolicy} + > + {form => ( +
+ form.setFieldValue('allowOverrides', newValue)} + disabled={!currentOrganization.viewerCanModifySchemaPolicy} + /> + +
+ )} +
+
+ ); +} + const OrganizationSettingsPageQuery = graphql(` query OrganizationSettingsPageQuery($organizationSlug: String!) { organization: organizationBySlug(organizationSlug: $organizationSlug) { ...SettingsPageRenderer_OrganizationFragment + ...OrganizationPolicySettings_OrganizationFragment viewerCanAccessSettings viewerCanManageAccessTokens } } `); -export const OrganizationSettingsPageEnum = z.enum(['general', 'access-tokens']); +export const OrganizationSettingsPageEnum = z.enum(['general', 'policy', 'access-tokens']); export type OrganizationSettingsSubPage = z.TypeOf; function SettingsPageContent(props: { @@ -510,6 +653,11 @@ function SettingsPageContent(props: { }); } + pages.push({ + key: 'policy', + title: 'Policy', + }); + if (currentOrganization?.viewerCanManageAccessTokens) { pages.push({ key: 'access-tokens', @@ -577,15 +725,20 @@ function SettingsPageContent(props: { })} - {resolvedPage.key === 'general' ? ( - - ) : null} - {resolvedPage.key === 'access-tokens' ? ( - - ) : null} +
+ {resolvedPage.key === 'general' ? ( + + ) : null} + {resolvedPage.key === 'policy' ? ( + + ) : null} + {resolvedPage.key === 'access-tokens' ? ( + + ) : null} +
diff --git a/packages/web/app/src/pages/project-policy.tsx b/packages/web/app/src/pages/project-policy.tsx deleted file mode 100644 index e90854d790..0000000000 --- a/packages/web/app/src/pages/project-policy.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useMutation, useQuery } from 'urql'; -import { Page, ProjectLayout } from '@/components/layouts/project'; -import { PolicySettings } from '@/components/policy/policy-settings'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { DocsLink } from '@/components/ui/docs-note'; -import { Meta } from '@/components/ui/meta'; -import { Subtitle, Title } from '@/components/ui/page'; -import { QueryError } from '@/components/ui/query-error'; -import { useToast } from '@/components/ui/use-toast'; -import { graphql } from '@/gql'; - -const ProjectPolicyPageQuery = graphql(` - query ProjectPolicyPageQuery($organizationSlug: String!, $projectSlug: String!) { - organization: organizationBySlug(organizationSlug: $organizationSlug) { - id - project: projectBySlug(projectSlug: $projectSlug) { - id - schemaPolicy { - id - updatedAt - ...PolicySettings_SchemaPolicyFragment - } - parentSchemaPolicy { - id - updatedAt - allowOverrides - rules { - rule { - id - } - } - } - viewerCanModifySchemaPolicy - } - } - } -`); - -const UpdateSchemaPolicyForProject = graphql(` - mutation UpdateSchemaPolicyForProject( - $selector: ProjectSelectorInput! - $policy: SchemaPolicyInput! - ) { - updateSchemaPolicyForProject(selector: $selector, policy: $policy) { - error { - message - } - ok { - project { - id - schemaPolicy { - id - updatedAt - ...PolicySettings_SchemaPolicyFragment - } - } - } - } - } -`); - -function ProjectPolicyContent(props: { organizationSlug: string; projectSlug: string }) { - const [mutation, mutate] = useMutation(UpdateSchemaPolicyForProject); - const [query] = useQuery({ - query: ProjectPolicyPageQuery, - variables: { - organizationSlug: props.organizationSlug, - projectSlug: props.projectSlug, - }, - requestPolicy: 'cache-and-network', - }); - const { toast } = useToast(); - - const currentOrganization = query.data?.organization; - const currentProject = currentOrganization?.project; - - if (query.error) { - return ( - - ); - } - - return ( -
-
- Project Schema Policy - - Schema Policies enable developers to define additional semantic checks on the GraphQL - schema. - -
- {currentProject && currentOrganization ? ( - - - Rules - - At the project level, policies can be defined to affect all targets, and override - policy configuration defined at the organization level. -
- - Learn more - -
-
- - {currentProject.parentSchemaPolicy === null || - currentProject.parentSchemaPolicy?.allowOverrides ? ( - r.rule.id)} - error={ - mutation.error?.message || - mutation.data?.updateSchemaPolicyForProject.error?.message - } - onSave={ - currentProject?.viewerCanModifySchemaPolicy - ? async newPolicy => { - await mutate({ - selector: { - organizationSlug: props.organizationSlug, - projectSlug: props.projectSlug, - }, - policy: newPolicy, - }).then(result => { - if (result.error || result.data?.updateSchemaPolicyForProject.error) { - toast({ - variant: 'destructive', - title: 'Error', - description: - result.error?.message || - result.data?.updateSchemaPolicyForProject.error?.message, - }); - } else { - toast({ - variant: 'default', - title: 'Success', - description: 'Policy updated successfully', - }); - } - }); - } - : null - } - currentState={currentProject.schemaPolicy} - /> - ) : ( -
-

!

- Organization settings does not allow projects to override policy. Please consult - your organization administrator. -
- )} -
-
- ) : null} -
- ); -} - -export function ProjectPolicyPage(props: { organizationSlug: string; projectSlug: string }) { - return ( - <> - - - - - - ); -} diff --git a/packages/web/app/src/pages/project-settings.tsx b/packages/web/app/src/pages/project-settings.tsx index c5cc66afe0..09ff757f47 100644 --- a/packages/web/app/src/pages/project-settings.tsx +++ b/packages/web/app/src/pages/project-settings.tsx @@ -1,19 +1,13 @@ -import { ReactElement, useCallback } from 'react'; +import { ReactElement, useCallback, useMemo } from 'react'; import { ArrowBigDownDashIcon, CheckIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { useMutation, useQuery } from 'urql'; import { z } from 'zod'; import { Page, ProjectLayout } from '@/components/layouts/project'; +import { PolicySettings } from '@/components/policy/policy-settings'; import { CompositionSettings } from '@/components/project/settings/composition'; import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { CardDescription } from '@/components/ui/card'; import { Dialog, DialogContent, @@ -27,16 +21,23 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component import { HiveLogo } from '@/components/ui/icon'; import { Input } from '@/components/ui/input'; 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 { ResourceDetails } from '@/components/ui/resource-details'; import { useToast } from '@/components/ui/use-toast'; import { env } from '@/env/frontend'; -import { graphql, useFragment } from '@/gql'; +import { FragmentType, graphql, useFragment } from '@/gql'; import { ProjectType } from '@/gql/graphql'; import { useRedirect } from '@/lib/access/common'; import { getDocsUrl } from '@/lib/docs-url'; import { useNotifications, useToggle } from '@/lib/hooks'; +import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRouter } from '@tanstack/react-router'; @@ -91,82 +92,76 @@ function GitHubIntegration(props: { } return ( - - - Use project's name in GitHub Check - - Prevents GitHub Check name collisions when running{' '} - - - $ hive schema:check --github - - - for more than one project. - - - -
-
-
-
Here's how it will look like in your CI pipeline.
-
- -
- -
- -
- {props.organizationSlug} > schema:check > staging -
-
— No changes
+ + + Prevents GitHub Check name collisions when running{' '} + + + $ hive schema:check --github + + + for more than one project. + + } + /> +
+
+
Here's how it will look like in your CI pipeline.
+
+
+ +
+
+ +
+ {props.organizationSlug} > schema:check > staging +
+
— No changes
-
- -
-
-
- -
- -
- -
- {props.organizationSlug} > schema:check > {props.projectSlug} > staging -
-
— No changes
+ +
+ +
+ +
+ +
+ {props.organizationSlug} > schema:check > {props.projectSlug} > staging
+
— No changes
-
- -
- - + +
+ ); } @@ -259,22 +254,25 @@ function ProjectSettingsPage_SlugForm(props: { organizationSlug: string; project return (
- - - Project Slug - - This is your project's URL namespace on Hive. Changing it{' '} - will invalidate any existing links to your project. -
- - You can read more about it in the documentation - -
-
- + + + This is your project's URL namespace on Hive. Changing it{' '} + will invalidate any existing links to your + project. +
+ + You can read more about it in the documentation + + + } + /> +
)} /> - - - - +
+
); } +function ProjectDelete(props: { organizationSlug: string; projectSlug: string }) { + const [isModalOpen, toggleModalOpen] = useToggle(); + + return ( + + + + Deleting an project will delete all the targets, schemas and data associated with it. + + + + This action is not reversible! You can find more information about + this process in the documentation + + + + } + /> + + + + ); +} + +const ProjectPolicySettings_ProjectFragment = graphql(` + fragment ProjectPolicySettings_ProjectFragment on Project { + id + slug + schemaPolicy { + id + updatedAt + ...PolicySettings_SchemaPolicyFragment + } + parentSchemaPolicy { + id + updatedAt + allowOverrides + rules { + rule { + id + } + } + } + viewerCanModifySchemaPolicy + } +`); + +const UpdateSchemaPolicyForProject = graphql(` + mutation UpdateSchemaPolicyForProject( + $selector: ProjectSelectorInput! + $policy: SchemaPolicyInput! + ) { + updateSchemaPolicyForProject(selector: $selector, policy: $policy) { + error { + message + } + ok { + project { + id + schemaPolicy { + id + updatedAt + ...PolicySettings_SchemaPolicyFragment + } + } + } + } + } +`); + +function ProjectPolicySettings(props: { + organizationSlug: string; + project: FragmentType; +}) { + const [mutation, mutate] = useMutation(UpdateSchemaPolicyForProject); + const { toast } = useToast(); + + const currentProject = useFragment(ProjectPolicySettings_ProjectFragment, props.project); + + return ( + + + + At the project level, policies can be defined to affect all targets, and override + policy configuration defined at the organization level. + + + + Learn more + + + + } + /> + {currentProject.parentSchemaPolicy === null || + currentProject.parentSchemaPolicy?.allowOverrides ? ( + r.rule.id)} + error={ + mutation.error?.message || mutation.data?.updateSchemaPolicyForProject.error?.message + } + onSave={ + currentProject?.viewerCanModifySchemaPolicy + ? async newPolicy => { + await mutate({ + selector: { + organizationSlug: props.organizationSlug, + projectSlug: currentProject.slug, + }, + policy: newPolicy, + }).then(result => { + if (result.error || result.data?.updateSchemaPolicyForProject.error) { + toast({ + variant: 'destructive', + title: 'Error', + description: + result.error?.message || + result.data?.updateSchemaPolicyForProject.error?.message, + }); + } else { + toast({ + variant: 'default', + title: 'Success', + description: 'Policy updated successfully', + }); + } + }); + } + : null + } + currentState={currentProject.schemaPolicy} + /> + ) : ( +
+

!

+ Organization settings does not allow projects to override policy. Please consult your + organization administrator. +
+ )} +
+ ); +} + const ProjectSettingsPage_OrganizationFragment = graphql(` fragment ProjectSettingsPage_OrganizationFragment on Organization { id @@ -321,6 +477,7 @@ const ProjectSettingsPage_ProjectFragment = graphql(` viewerCanDelete viewerCanModifySettings ...CompositionSettings_ProjectFragment + ...ProjectPolicySettings_ProjectFragment } `); @@ -336,8 +493,12 @@ const ProjectSettingsPageQuery = graphql(` } `); -function ProjectSettingsContent(props: { organizationSlug: string; projectSlug: string }) { - const [isModalOpen, toggleModalOpen] = useToggle(); +function ProjectSettingsContent(props: { + organizationSlug: string; + projectSlug: string; + page?: ProjectSettingsSubPage; +}) { + const router = useRouter(); const [query] = useQuery({ query: ProjectSettingsPageQuery, variables: { @@ -369,7 +530,30 @@ function ProjectSettingsContent(props: { organizationSlug: string; projectSlug: entity: project, }); - if (project?.viewerCanModifySettings === false) { + const subPages = useMemo(() => { + const pages: Array<{ + key: ProjectSettingsSubPage; + title: string; + }> = []; + + if (project?.viewerCanModifySettings) { + pages.push({ + key: 'general', + title: 'General', + }); + } + + pages.push({ + key: 'policy', + title: 'Policy', + }); + + return pages; + }, [project]); + + const resolvedPage = props.page ? subPages.find(page => page.key === props.page) : subPages.at(0); + + if (!resolvedPage || !organization || !project) { return null; } @@ -384,66 +568,74 @@ function ProjectSettingsContent(props: { organizationSlug: string; projectSlug: } return ( -
-
- Settings - Manage your project settings -
-
- {project && organization ? ( - <> - - - {query.data?.isGitHubIntegrationFeatureEnabled && - !project.isProjectNameInGitHubCheckEnabled ? ( - - ) : null} - - {project.type === ProjectType.Federation ? ( - - ) : null} - - {project.viewerCanDelete && ( - - - Delete Project - - Deleting an project will delete all the targets, schemas and data associated - with it. -
- - This action is not reversible! You can find more information - about this process in the documentation - -
-
- - - -
+ + + {subPages.map(subPage => ( +
-
+ > + {subPage.title} + + ))} + + +
+ {resolvedPage.key === 'general' ? ( + <> + + + {query.data?.isGitHubIntegrationFeatureEnabled && + !project.isProjectNameInGitHubCheckEnabled ? ( + + ) : null} + + {project.type === ProjectType.Federation ? ( + + ) : null} + + {project.viewerCanDelete ? ( + + ) : null} + + ) : null} + {resolvedPage.key === 'policy' ? ( + + ) : null} +
+
+ ); } -export function ProjectSettingsPage(props: { organizationSlug: string; projectSlug: string }) { +export const ProjectSettingsPageEnum = z.enum(['general', 'policy']); + +export type ProjectSettingsSubPage = z.TypeOf; + +export function ProjectSettingsPage(props: { + organizationSlug: string; + projectSlug: string; + page?: ProjectSettingsSubPage; +}) { return ( <> @@ -456,6 +648,7 @@ export function ProjectSettingsPage(props: { organizationSlug: string; projectSl diff --git a/packages/web/app/src/router.tsx b/packages/web/app/src/router.tsx index a561d5c936..5333c5e8f5 100644 --- a/packages/web/app/src/router.tsx +++ b/packages/web/app/src/router.tsx @@ -46,7 +46,6 @@ import { OrganizationIndexRouteSearch, OrganizationPage } from './pages/organiza import { JoinOrganizationPage } from './pages/organization-join'; import { OrganizationMembersPage } from './pages/organization-members'; import { NewOrgPage } from './pages/organization-new'; -import { OrganizationPolicyPage } from './pages/organization-policy'; import { OrganizationSettingsPage, OrganizationSettingsPageEnum, @@ -58,8 +57,7 @@ import { OrganizationSupportTicketPage } from './pages/organization-support-tick import { OrganizationTransferPage } from './pages/organization-transfer'; import { ProjectIndexRouteSearch, ProjectPage } from './pages/project'; import { ProjectAlertsPage } from './pages/project-alerts'; -import { ProjectPolicyPage } from './pages/project-policy'; -import { ProjectSettingsPage } from './pages/project-settings'; +import { ProjectSettingsPage, ProjectSettingsPageEnum } from './pages/project-settings'; import { TargetPage } from './pages/target'; import { TargetAppVersionPage } from './pages/target-app-version'; import { TargetAppsPage } from './pages/target-apps'; @@ -433,15 +431,6 @@ const organizationSubscriptionManageRoute = createRoute({ }, }); -const organizationPolicyRoute = createRoute({ - getParentRoute: () => organizationRoute, - path: 'view/policy', - component: function OrganizationPolicyRoute() { - const { organizationSlug } = organizationPolicyRoute.useParams(); - return ; - }, -}); - const OrganizationSettingRouteSearch = z.object({ page: OrganizationSettingsPageEnum.default('general').optional(), }); @@ -516,21 +505,27 @@ const projectIndexRoute = createRoute({ }, }); +const ProjectSettingsRouteSearch = z.object({ + page: ProjectSettingsPageEnum.default('general').optional(), +}); + const projectSettingsRoute = createRoute({ getParentRoute: () => projectRoute, path: 'view/settings', + validateSearch(search) { + return ProjectSettingsRouteSearch.parse(search); + }, component: function ProjectSettingsRoute() { const { organizationSlug, projectSlug } = projectSettingsRoute.useParams(); - return ; - }, -}); + const { page } = projectSettingsRoute.useSearch(); -const projectPolicyRoute = createRoute({ - getParentRoute: () => projectRoute, - path: 'view/policy', - component: function ProjectPolicyRoute() { - const { organizationSlug, projectSlug } = projectPolicyRoute.useParams(); - return ; + return ( + + ); }, }); @@ -950,15 +945,9 @@ const routeTree = root.addChildren([ organizationSubscriptionManageRoute, organizationSubscriptionManageLegacyRoute, organizationMembersRoute, - organizationPolicyRoute, organizationSettingsRoute, ]), - projectRoute.addChildren([ - projectIndexRoute, - projectSettingsRoute, - projectPolicyRoute, - projectAlertsRoute, - ]), + projectRoute.addChildren([projectIndexRoute, projectSettingsRoute, projectAlertsRoute]), targetRoute.addChildren([ targetIndexRoute, targetSettingsRoute,