mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
feat(app): move policy page into settings (#7107)
This commit is contained in:
parent
d9067e86b4
commit
252507bdc3
8 changed files with 736 additions and 789 deletions
|
|
@ -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({
|
|||
</Link>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger variant="menu" value={Page.Policy} asChild>
|
||||
<TabsTrigger variant="menu" value={Page.Settings} asChild>
|
||||
<Link
|
||||
to="/$organizationSlug/view/policy"
|
||||
to="/$organizationSlug/view/settings"
|
||||
params={{ organizationSlug: currentOrganization.slug }}
|
||||
>
|
||||
Policy
|
||||
Settings
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
{currentOrganization.viewerCanAccessSettings && (
|
||||
<TabsTrigger variant="menu" value={Page.Settings} asChild>
|
||||
<Link
|
||||
to="/$organizationSlug/view/settings"
|
||||
params={{ organizationSlug: currentOrganization.slug }}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{currentOrganization.viewerCanManageSupportTickets && (
|
||||
<TabsTrigger variant="menu" value={Page.Support} asChild>
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { ProjectSelector } from './project-selector';
|
|||
export enum Page {
|
||||
Targets = 'targets',
|
||||
Alerts = 'alerts',
|
||||
Policy = 'policy',
|
||||
Settings = 'settings',
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +51,6 @@ const ProjectLayoutQuery = graphql(`
|
|||
viewerCanModifySchemaPolicy
|
||||
viewerCanCreateTarget
|
||||
viewerCanModifyAlerts
|
||||
viewerCanModifySettings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,30 +140,17 @@ export function ProjectLayout({
|
|||
</Link>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger variant="menu" value={Page.Policy} asChild>
|
||||
<TabsTrigger variant="menu" value={Page.Settings} asChild>
|
||||
<Link
|
||||
to="/$organizationSlug/$projectSlug/view/policy"
|
||||
to="/$organizationSlug/$projectSlug/view/settings"
|
||||
params={{
|
||||
organizationSlug: props.organizationSlug,
|
||||
projectSlug: props.projectSlug,
|
||||
}}
|
||||
>
|
||||
Policy
|
||||
Settings
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
{currentProject.viewerCanModifySettings && (
|
||||
<TabsTrigger variant="menu" value={Page.Settings} asChild>
|
||||
<Link
|
||||
to="/$organizationSlug/$projectSlug/view/settings"
|
||||
params={{
|
||||
organizationSlug: props.organizationSlug,
|
||||
projectSlug: props.projectSlug,
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<a id="composition">Schema Composition</a>
|
||||
</CardTitle>
|
||||
<CardDescription>Configure how your schemas are composed.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle={<a id="composition">Schema Composition</a>}
|
||||
description={<CardDescription>Configure how your schemas are composed.</CardDescription>}
|
||||
/>
|
||||
<div>
|
||||
{projectQuery.fetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
|
|
@ -149,7 +148,7 @@ export const CompositionSettings = (props: {
|
|||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 <QueryError organizationSlug={props.organizationSlug} error={query.error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<OrganizationLayout
|
||||
page={Page.Policy}
|
||||
organizationSlug={props.organizationSlug}
|
||||
className="flex flex-col gap-y-10"
|
||||
>
|
||||
<div>
|
||||
<div className="py-6">
|
||||
<Title>Organization Schema Policy</Title>
|
||||
<Subtitle>
|
||||
Schema Policies enable developers to define additional semantic checks on the GraphQL
|
||||
schema.
|
||||
</Subtitle>
|
||||
</div>
|
||||
{currentOrganization ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Rules</CardTitle>
|
||||
<CardDescription>
|
||||
At the organizational level, policies can be defined to affect all projects and
|
||||
targets.
|
||||
<br />
|
||||
At the project level, policies can be overridden or extended.
|
||||
<br />
|
||||
<DocsLink className="text-muted-foreground" href="/features/schema-policy">
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PolicySettings
|
||||
saving={mutation.fetching}
|
||||
error={
|
||||
mutation.error?.message ||
|
||||
mutation.data?.updateSchemaPolicyForOrganization.error?.message
|
||||
}
|
||||
onSave={
|
||||
currentOrganization.viewerCanModifySchemaPolicy
|
||||
? async (newPolicy, allowOverrides) => {
|
||||
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 => (
|
||||
<div className="flex items-center pl-1 pt-2">
|
||||
<Checkbox
|
||||
id="allowOverrides"
|
||||
checked={form.values.allowOverrides}
|
||||
value="allowOverrides"
|
||||
onCheckedChange={newValue => form.setFieldValue('allowOverrides', newValue)}
|
||||
disabled={!currentOrganization.viewerCanModifySchemaPolicy}
|
||||
/>
|
||||
<label
|
||||
htmlFor="allowOverrides"
|
||||
className="ml-2 inline-block text-sm text-gray-300"
|
||||
>
|
||||
Allow projects to override or disable rules
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</PolicySettings>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
</OrganizationLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export function OrganizationPolicyPage(props: { organizationSlug: string }): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Organization Schema Policy" />
|
||||
<PolicyPageContent organizationSlug={props.organizationSlug} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<typeof SlugFormSchema>;
|
||||
|
||||
const SettingsPageRenderer = (props: {
|
||||
const OrganizationSettingsContent = (props: {
|
||||
organization: FragmentType<typeof SettingsPageRenderer_OrganizationFragment>;
|
||||
organizationSlug: string;
|
||||
}) => {
|
||||
|
|
@ -263,224 +264,366 @@ const SettingsPageRenderer = (props: {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="space-y-12">
|
||||
<ResourceDetails id={organization.id} />
|
||||
{organization.viewerCanModifySlug && (
|
||||
<Form {...slugForm}>
|
||||
<form onSubmit={slugForm.handleSubmit(onSlugFormSubmit)}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Organization Slug</CardTitle>
|
||||
<CardDescription>
|
||||
This is your organization's URL namespace on GraphQL Hive. Changing it{' '}
|
||||
<span className="font-bold">will</span> invalidate any existing links to your
|
||||
organization.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#change-slug-of-organization"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<FormField
|
||||
control={slugForm.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex items-center">
|
||||
<div className="border-input text-muted-foreground h-10 rounded-md rounded-r-none border-y border-l bg-gray-900 px-3 py-2 text-sm">
|
||||
{env.appBaseUrl.replace(/https?:\/\//i, '')}/
|
||||
</div>
|
||||
<Input placeholder="slug" className="w-48 rounded-l-none" {...field} />
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Organization Slug"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
This is your organization's URL namespace on GraphQL Hive. Changing it{' '}
|
||||
<span className="font-bold">will</span> invalidate any existing links to your
|
||||
organization.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#change-slug-of-organization"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
control={slugForm.control}
|
||||
name="slug"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className="flex items-center">
|
||||
<div className="border-input text-muted-foreground h-10 rounded-md rounded-r-none border-y border-l bg-gray-900 px-3 py-2 text-sm">
|
||||
{env.appBaseUrl.replace(/https?:\/\//i, '')}/
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button disabled={slugForm.formState.isSubmitting} className="px-10" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Input placeholder="slug" className="w-48 rounded-l-none" {...field} />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button disabled={slugForm.formState.isSubmitting} className="px-10" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</SubPageLayout>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{organization.viewerCanManageOIDCIntegration && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Single Sign On Provider</CardTitle>
|
||||
<CardDescription>
|
||||
Link your Hive organization to a single-sign-on provider such as Okta or Microsoft
|
||||
Entra ID via OpenID Connect.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/sso-oidc-provider"
|
||||
>
|
||||
Instructions for connecting your provider.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-y-4 text-gray-500">
|
||||
<OIDCIntegrationSection organization={organization} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Single Sign On Provider"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Link your Hive organization to a single-sign-on provider such as Okta or Microsoft
|
||||
Entra ID via OpenID Connect.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/sso-oidc-provider"
|
||||
>
|
||||
Instructions for connecting your provider.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="text-gray-500">
|
||||
<OIDCIntegrationSection organization={organization} />
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
)}
|
||||
|
||||
{organization.viewerCanModifySlackIntegration && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Slack Integration</CardTitle>
|
||||
<CardDescription>
|
||||
Link your Hive organization with Slack for schema change notifications.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#slack"
|
||||
>
|
||||
Learn more.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SlackIntegrationSection organization={organization} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Slack Integration"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Link your Hive organization with Slack for schema change notifications.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#slack"
|
||||
>
|
||||
Learn more.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SlackIntegrationSection organization={organization} />
|
||||
</SubPageLayout>
|
||||
)}
|
||||
|
||||
{organization.viewerCanModifyGitHubIntegration && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>GitHub Integration</CardTitle>
|
||||
<CardDescription>
|
||||
Link your Hive organization with GitHub.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#github"
|
||||
>
|
||||
Learn more.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GitHubIntegrationSection organization={organization} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="GitHub Integration"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>Link your Hive organization with GitHub.</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#github"
|
||||
>
|
||||
Learn more.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<GitHubIntegrationSection organization={organization} />
|
||||
</SubPageLayout>
|
||||
)}
|
||||
|
||||
{organization.viewerCanTransferOwnership && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transfer Ownership</CardTitle>
|
||||
<CardDescription>
|
||||
<strong>You are currently the owner of the organization.</strong> You can transfer the
|
||||
organization to another member of the organization, or to an external user.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#transfer-ownership"
|
||||
>
|
||||
Learn more about the process
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Button variant="destructive" onClick={toggleTransferModalOpen} className="px-5">
|
||||
Transfer Ownership
|
||||
</Button>
|
||||
<TransferOrganizationOwnershipModal
|
||||
isOpen={isTransferModalOpen}
|
||||
toggleModalOpen={toggleTransferModalOpen}
|
||||
organization={organization}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Transfer Ownership"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
<strong>You are currently the owner of the organization.</strong> You can transfer
|
||||
the organization to another member of the organization, or to an external user.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#transfer-ownership"
|
||||
>
|
||||
Learn more about the process
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="destructive" onClick={toggleTransferModalOpen} className="px-5">
|
||||
Transfer Ownership
|
||||
</Button>
|
||||
<TransferOrganizationOwnershipModal
|
||||
isOpen={isTransferModalOpen}
|
||||
toggleModalOpen={toggleTransferModalOpen}
|
||||
organization={organization}
|
||||
/>
|
||||
</SubPageLayout>
|
||||
)}
|
||||
|
||||
{organization.viewerCanDelete && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Delete Organization</CardTitle>
|
||||
<CardDescription>
|
||||
Deleting an organization will delete all the projects, targets, schemas and data
|
||||
associated with it.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#delete-an-organization"
|
||||
>
|
||||
<strong>This action is not reversible!</strong> You can find more information about
|
||||
this process in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button variant="destructive" onClick={toggleDeleteModalOpen} className="px-5">
|
||||
Delete Organization
|
||||
</Button>
|
||||
<DeleteOrganizationModal
|
||||
organizationSlug={props.organizationSlug}
|
||||
isOpen={isDeleteModalOpen}
|
||||
toggleModalOpen={toggleDeleteModalOpen}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Delete Organization"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Deleting an organization will delete all the projects, targets, schemas and data
|
||||
associated with it.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/organizations#delete-an-organization"
|
||||
>
|
||||
<span>
|
||||
<strong>This action is not reversible!</strong> You can find more information
|
||||
about this process in the documentation
|
||||
</span>
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="destructive" onClick={toggleDeleteModalOpen} className="px-5">
|
||||
Delete Organization
|
||||
</Button>
|
||||
<DeleteOrganizationModal
|
||||
organizationSlug={props.organizationSlug}
|
||||
isOpen={isDeleteModalOpen}
|
||||
toggleModalOpen={toggleDeleteModalOpen}
|
||||
/>
|
||||
</SubPageLayout>
|
||||
)}
|
||||
|
||||
{organization.viewerCanExportAuditLogs && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audit Logs</CardTitle>
|
||||
<CardDescription>
|
||||
View a history of changes made to the organization settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Button variant="default" onClick={toggleAuditLogsModalOpen} className="px-5">
|
||||
Export Audit Logs
|
||||
</Button>
|
||||
<AuditLogsOrganizationModal
|
||||
organizationSlug={organization.slug}
|
||||
isOpen={isAuditLogsModalOpen}
|
||||
toggleModalOpen={toggleAuditLogsModalOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Audit Logs"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
View a history of changes made to the organization settings.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink className="text-muted-foreground text-sm" href="/management/audit-logs">
|
||||
Learn more.
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="default" onClick={toggleAuditLogsModalOpen} className="px-5">
|
||||
Export Audit Logs
|
||||
</Button>
|
||||
<AuditLogsOrganizationModal
|
||||
organizationSlug={organization.slug}
|
||||
isOpen={isAuditLogsModalOpen}
|
||||
toggleModalOpen={toggleAuditLogsModalOpen}
|
||||
/>
|
||||
</SubPageLayout>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<typeof OrganizationPolicySettings_OrganizationFragment>;
|
||||
}) {
|
||||
const [mutation, mutate] = useMutation(UpdateSchemaPolicyForOrganization);
|
||||
const { toast } = useToast();
|
||||
|
||||
const currentOrganization = useFragment(
|
||||
OrganizationPolicySettings_OrganizationFragment,
|
||||
props.organization,
|
||||
);
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Rules"
|
||||
description={
|
||||
<CardDescription>
|
||||
At the organizational level, policies can be defined to affect all projects and targets.
|
||||
<br />
|
||||
At the project level, policies can be overridden or extended.
|
||||
<br />
|
||||
<DocsLink className="text-muted-foreground" href="/features/schema-policy">
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
}
|
||||
/>
|
||||
<PolicySettings
|
||||
saving={mutation.fetching}
|
||||
error={
|
||||
mutation.error?.message || mutation.data?.updateSchemaPolicyForOrganization.error?.message
|
||||
}
|
||||
onSave={
|
||||
currentOrganization.viewerCanModifySchemaPolicy
|
||||
? async (newPolicy, allowOverrides) => {
|
||||
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 => (
|
||||
<div className="flex items-center pl-1 pt-2">
|
||||
<Checkbox
|
||||
id="allowOverrides"
|
||||
checked={form.values.allowOverrides}
|
||||
value="allowOverrides"
|
||||
onCheckedChange={newValue => form.setFieldValue('allowOverrides', newValue)}
|
||||
disabled={!currentOrganization.viewerCanModifySchemaPolicy}
|
||||
/>
|
||||
<label htmlFor="allowOverrides" className="ml-2 inline-block text-sm text-gray-300">
|
||||
Allow projects to override or disable rules
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</PolicySettings>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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<typeof OrganizationSettingsPageEnum>;
|
||||
|
||||
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: {
|
|||
})}
|
||||
</NavLayout>
|
||||
<PageLayoutContent>
|
||||
{resolvedPage.key === 'general' ? (
|
||||
<SettingsPageRenderer
|
||||
organizationSlug={props.organizationSlug}
|
||||
organization={currentOrganization}
|
||||
/>
|
||||
) : null}
|
||||
{resolvedPage.key === 'access-tokens' ? (
|
||||
<AccessTokensSubPage organizationSlug={props.organizationSlug} />
|
||||
) : null}
|
||||
<div className="space-y-12">
|
||||
{resolvedPage.key === 'general' ? (
|
||||
<OrganizationSettingsContent
|
||||
organizationSlug={props.organizationSlug}
|
||||
organization={currentOrganization}
|
||||
/>
|
||||
) : null}
|
||||
{resolvedPage.key === 'policy' ? (
|
||||
<OrganizationPolicySettings organization={currentOrganization} />
|
||||
) : null}
|
||||
{resolvedPage.key === 'access-tokens' ? (
|
||||
<AccessTokensSubPage organizationSlug={props.organizationSlug} />
|
||||
) : null}
|
||||
</div>
|
||||
</PageLayoutContent>
|
||||
</PageLayout>
|
||||
</OrganizationLayout>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<QueryError
|
||||
organizationSlug={props.organizationSlug}
|
||||
error={query.error}
|
||||
showLogoutButton={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="py-6">
|
||||
<Title>Project Schema Policy</Title>
|
||||
<Subtitle>
|
||||
Schema Policies enable developers to define additional semantic checks on the GraphQL
|
||||
schema.
|
||||
</Subtitle>
|
||||
</div>
|
||||
{currentProject && currentOrganization ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Rules</CardTitle>
|
||||
<CardDescription>
|
||||
At the project level, policies can be defined to affect all targets, and override
|
||||
policy configuration defined at the organization level.
|
||||
<br />
|
||||
<DocsLink href="/features/schema-policy" className="text-muted-foreground text-sm">
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{currentProject.parentSchemaPolicy === null ||
|
||||
currentProject.parentSchemaPolicy?.allowOverrides ? (
|
||||
<PolicySettings
|
||||
saving={mutation.fetching}
|
||||
rulesInParent={currentProject.parentSchemaPolicy?.rules.map(r => 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}
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-1 text-sm font-bold text-gray-400">
|
||||
<p className="mr-4 inline-block text-orange-500">!</p>
|
||||
Organization settings does not allow projects to override policy. Please consult
|
||||
your organization administrator.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectPolicyPage(props: { organizationSlug: string; projectSlug: string }) {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Project Schema Policy" />
|
||||
<ProjectLayout
|
||||
organizationSlug={props.organizationSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
page={Page.Policy}
|
||||
className="flex flex-col gap-y-10"
|
||||
>
|
||||
<ProjectPolicyContent
|
||||
organizationSlug={props.organizationSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
</ProjectLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Use project's name in GitHub Check</CardTitle>
|
||||
<CardDescription>
|
||||
Prevents GitHub Check name collisions when running{' '}
|
||||
<a href={docksLink}>
|
||||
<span className="mx-1 text-orange-700 hover:underline hover:underline-offset-4">
|
||||
$ hive schema:check --github
|
||||
</span>
|
||||
</a>
|
||||
for more than one project.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-muted-foreground mb-4 flex flex-row items-center justify-between gap-x-4 rounded-sm text-sm">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<div className="mb-4">Here's how it will look like in your CI pipeline.</div>
|
||||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
{props.organizationSlug} > schema:check > staging
|
||||
</div>
|
||||
<div className="text-gray-500">— No changes</div>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Use project's name in GitHub Check"
|
||||
description={
|
||||
<CardDescription>
|
||||
Prevents GitHub Check name collisions when running{' '}
|
||||
<a href={docksLink}>
|
||||
<span className="mx-1 text-orange-700 hover:underline hover:underline-offset-4">
|
||||
$ hive schema:check --github
|
||||
</span>
|
||||
</a>
|
||||
for more than one project.
|
||||
</CardDescription>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
<div>Here's how it will look like in your CI pipeline.</div>
|
||||
<div className="my-8 flex w-fit flex-col gap-y-1">
|
||||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ArrowBigDownDashIcon className="size-6" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
{props.organizationSlug} > schema:check > {props.projectSlug} > staging
|
||||
</div>
|
||||
<div className="text-gray-500">— No changes</div>
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
{props.organizationSlug} > schema:check > staging
|
||||
</div>
|
||||
<div className="text-gray-500">— No changes</div>
|
||||
</div>
|
||||
<ArrowBigDownDashIcon className="size-6 self-center" />
|
||||
<div className="flex items-center gap-x-2 pl-1">
|
||||
<CheckIcon className="size-4 text-emerald-500" />
|
||||
<div className="flex size-6 items-center justify-center rounded-sm bg-white">
|
||||
<HiveLogo className="size-4/5" />
|
||||
</div>
|
||||
|
||||
<div className="font-semibold text-[#adbac7]">
|
||||
{props.organizationSlug} > schema:check > {props.projectSlug} > staging
|
||||
</div>
|
||||
<div className="text-gray-500">— No changes</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pr-6">
|
||||
<Button
|
||||
disabled={ghCheckMutation.fetching}
|
||||
onClick={() => {
|
||||
void ghCheckMutate({
|
||||
input: {
|
||||
organizationSlug: props.organizationSlug,
|
||||
projectSlug: props.projectSlug,
|
||||
},
|
||||
}).then(
|
||||
result => {
|
||||
if (result.error) {
|
||||
notify('Failed to enable', 'error');
|
||||
} else {
|
||||
notify('Migration completed', 'success');
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
notify('Failed to enable', 'error');
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
I want to migrate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
disabled={ghCheckMutation.fetching}
|
||||
onClick={() => {
|
||||
void ghCheckMutate({
|
||||
input: {
|
||||
organizationSlug: props.organizationSlug,
|
||||
projectSlug: props.projectSlug,
|
||||
},
|
||||
}).then(
|
||||
result => {
|
||||
if (result.error) {
|
||||
notify('Failed to enable', 'error');
|
||||
} else {
|
||||
notify('Migration completed', 'success');
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
notify('Failed to enable', 'error');
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
I want to migrate
|
||||
</Button>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -259,22 +254,25 @@ function ProjectSettingsPage_SlugForm(props: { organizationSlug: string; project
|
|||
return (
|
||||
<Form {...slugForm}>
|
||||
<form onSubmit={slugForm.handleSubmit(onSlugFormSubmit)}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Project Slug</CardTitle>
|
||||
<CardDescription>
|
||||
This is your project's URL namespace on Hive. Changing it{' '}
|
||||
<span className="font-bold">will</span> invalidate any existing links to your project.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/projects#change-slug-of-a-project"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Project Slug"
|
||||
description={
|
||||
<CardDescription>
|
||||
This is your project's URL namespace on Hive. Changing it{' '}
|
||||
<span className="font-bold">will</span> invalidate any existing links to your
|
||||
project.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/projects#change-slug-of-a-project"
|
||||
>
|
||||
You can read more about it in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<FormField
|
||||
control={slugForm.control}
|
||||
name="slug"
|
||||
|
|
@ -292,18 +290,176 @@ function ProjectSettingsPage_SlugForm(props: { organizationSlug: string; project
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button disabled={slugForm.formState.isSubmitting} className="px-10" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</SubPageLayout>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectDelete(props: { organizationSlug: string; projectSlug: string }) {
|
||||
const [isModalOpen, toggleModalOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Delete Project"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
Deleting an project will delete all the targets, schemas and data associated with it.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/projects#delete-a-project"
|
||||
>
|
||||
<strong>This action is not reversible!</strong> You can find more information about
|
||||
this process in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="destructive" onClick={toggleModalOpen}>
|
||||
Delete Project
|
||||
</Button>
|
||||
<DeleteProjectModal
|
||||
projectSlug={props.projectSlug}
|
||||
organizationSlug={props.organizationSlug}
|
||||
isOpen={isModalOpen}
|
||||
toggleModalOpen={toggleModalOpen}
|
||||
/>
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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<typeof ProjectPolicySettings_ProjectFragment>;
|
||||
}) {
|
||||
const [mutation, mutate] = useMutation(UpdateSchemaPolicyForProject);
|
||||
const { toast } = useToast();
|
||||
|
||||
const currentProject = useFragment(ProjectPolicySettings_ProjectFragment, props.project);
|
||||
|
||||
return (
|
||||
<SubPageLayout>
|
||||
<SubPageLayoutHeader
|
||||
subPageTitle="Rules"
|
||||
description={
|
||||
<>
|
||||
<CardDescription>
|
||||
At the project level, policies can be defined to affect all targets, and override
|
||||
policy configuration defined at the organization level.
|
||||
</CardDescription>
|
||||
<CardDescription>
|
||||
<DocsLink href="/features/schema-policy" className="text-muted-foreground text-sm">
|
||||
Learn more
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{currentProject.parentSchemaPolicy === null ||
|
||||
currentProject.parentSchemaPolicy?.allowOverrides ? (
|
||||
<PolicySettings
|
||||
saving={mutation.fetching}
|
||||
rulesInParent={currentProject.parentSchemaPolicy?.rules.map(r => 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}
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-1 text-sm font-bold text-gray-400">
|
||||
<p className="mr-4 inline-block text-orange-500">!</p>
|
||||
Organization settings does not allow projects to override policy. Please consult your
|
||||
organization administrator.
|
||||
</div>
|
||||
)}
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div className="py-6">
|
||||
<Title>Settings</Title>
|
||||
<Subtitle>Manage your project settings</Subtitle>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{project && organization ? (
|
||||
<>
|
||||
<ResourceDetails id={project.id} />
|
||||
<ProjectSettingsPage_SlugForm
|
||||
organizationSlug={props.organizationSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
{query.data?.isGitHubIntegrationFeatureEnabled &&
|
||||
!project.isProjectNameInGitHubCheckEnabled ? (
|
||||
<GitHubIntegration organizationSlug={organization.slug} projectSlug={project.slug} />
|
||||
) : null}
|
||||
|
||||
{project.type === ProjectType.Federation ? (
|
||||
<CompositionSettings project={project} organization={organization} />
|
||||
) : null}
|
||||
|
||||
{project.viewerCanDelete && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Delete Project</CardTitle>
|
||||
<CardDescription>
|
||||
Deleting an project will delete all the targets, schemas and data associated
|
||||
with it.
|
||||
<br />
|
||||
<DocsLink
|
||||
className="text-muted-foreground text-sm"
|
||||
href="/management/projects#delete-a-project"
|
||||
>
|
||||
<strong>This action is not reversible!</strong> You can find more information
|
||||
about this process in the documentation
|
||||
</DocsLink>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button variant="destructive" onClick={toggleModalOpen}>
|
||||
Delete Project
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<PageLayout>
|
||||
<NavLayout>
|
||||
{subPages.map(subPage => (
|
||||
<Button
|
||||
key={subPage.key}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
void router.navigate({
|
||||
search: {
|
||||
page: subPage.key,
|
||||
},
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
resolvedPage.key === subPage.key
|
||||
? 'bg-muted hover:bg-muted'
|
||||
: 'hover:bg-transparent hover:underline',
|
||||
'w-full justify-start text-left',
|
||||
)}
|
||||
<DeleteProjectModal
|
||||
projectSlug={props.projectSlug}
|
||||
organizationSlug={props.organizationSlug}
|
||||
isOpen={isModalOpen}
|
||||
toggleModalOpen={toggleModalOpen}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{subPage.title}
|
||||
</Button>
|
||||
))}
|
||||
</NavLayout>
|
||||
<PageLayoutContent>
|
||||
<div className="space-y-12">
|
||||
{resolvedPage.key === 'general' ? (
|
||||
<>
|
||||
<ResourceDetails id={project.id} />
|
||||
<ProjectSettingsPage_SlugForm
|
||||
organizationSlug={props.organizationSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
/>
|
||||
{query.data?.isGitHubIntegrationFeatureEnabled &&
|
||||
!project.isProjectNameInGitHubCheckEnabled ? (
|
||||
<GitHubIntegration
|
||||
organizationSlug={organization.slug}
|
||||
projectSlug={project.slug}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{project.type === ProjectType.Federation ? (
|
||||
<CompositionSettings project={project} organization={organization} />
|
||||
) : null}
|
||||
|
||||
{project.viewerCanDelete ? (
|
||||
<ProjectDelete projectSlug={project.slug} organizationSlug={organization.slug} />
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{resolvedPage.key === 'policy' ? (
|
||||
<ProjectPolicySettings organizationSlug={organization.slug} project={project} />
|
||||
) : null}
|
||||
</div>
|
||||
</PageLayoutContent>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectSettingsPage(props: { organizationSlug: string; projectSlug: string }) {
|
||||
export const ProjectSettingsPageEnum = z.enum(['general', 'policy']);
|
||||
|
||||
export type ProjectSettingsSubPage = z.TypeOf<typeof ProjectSettingsPageEnum>;
|
||||
|
||||
export function ProjectSettingsPage(props: {
|
||||
organizationSlug: string;
|
||||
projectSlug: string;
|
||||
page?: ProjectSettingsSubPage;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Meta title="Project settings" />
|
||||
|
|
@ -456,6 +648,7 @@ export function ProjectSettingsPage(props: { organizationSlug: string; projectSl
|
|||
<ProjectSettingsContent
|
||||
organizationSlug={props.organizationSlug}
|
||||
projectSlug={props.projectSlug}
|
||||
page={props.page}
|
||||
/>
|
||||
</ProjectLayout>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 <OrganizationPolicyPage organizationSlug={organizationSlug} />;
|
||||
},
|
||||
});
|
||||
|
||||
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 <ProjectSettingsPage organizationSlug={organizationSlug} projectSlug={projectSlug} />;
|
||||
},
|
||||
});
|
||||
const { page } = projectSettingsRoute.useSearch();
|
||||
|
||||
const projectPolicyRoute = createRoute({
|
||||
getParentRoute: () => projectRoute,
|
||||
path: 'view/policy',
|
||||
component: function ProjectPolicyRoute() {
|
||||
const { organizationSlug, projectSlug } = projectPolicyRoute.useParams();
|
||||
return <ProjectPolicyPage organizationSlug={organizationSlug} projectSlug={projectSlug} />;
|
||||
return (
|
||||
<ProjectSettingsPage
|
||||
organizationSlug={organizationSlug}
|
||||
projectSlug={projectSlug}
|
||||
page={page}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue