mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
feat: remove the legacy routes (#81)
This commit is contained in:
parent
4e23275fab
commit
5af93b885e
14 changed files with 0 additions and 1460 deletions
|
|
@ -1,30 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { Versions } from '@/components/target/history/Versions';
|
|
||||||
|
|
||||||
const HistoryView: React.FC = () => {
|
|
||||||
const router = useRouteSelector();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title="Schema History" subtitle="A list of published changes for your GraphQL schema.">
|
|
||||||
<div tw="flex flex-row h-full">
|
|
||||||
<div tw="flex-grow overflow-x-auto divide-y divide-gray-200">
|
|
||||||
<Versions
|
|
||||||
selector={{
|
|
||||||
organization: router.organizationId,
|
|
||||||
project: router.projectId,
|
|
||||||
target: router.targetId,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TargetHistory() {
|
|
||||||
return <TargetView title="History">{() => <HistoryView />}</TargetView>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import tw, { styled } from 'twin.macro';
|
|
||||||
import formatDate from 'date-fns/format';
|
|
||||||
import { useQuery } from 'urql';
|
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
|
||||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
|
||||||
import { CompareDocument, OrganizationFieldsFragment } from '@/graphql';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { TextToggle } from '@/components/common/Toogle';
|
|
||||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { Compare, View } from '@/components/target/history/Compare';
|
|
||||||
import { MarkAsValid } from '@/components/target/history/MarkAsValid';
|
|
||||||
import { useTargetAccess, TargetAccessScope } from '@/lib/access/target';
|
|
||||||
|
|
||||||
const Value = tw.div`text-base text-gray-900 dark:text-white`;
|
|
||||||
const ValueLabel = tw.div`text-xs text-gray-500 dark:text-gray-400`;
|
|
||||||
const Status = styled.span(({ valid }: { valid?: boolean }) => [
|
|
||||||
tw`mx-auto my-2 block w-2 h-2 rounded-full`,
|
|
||||||
valid ? tw`bg-emerald-400` : tw`bg-red-400`,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const VersionView: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ organization }) => {
|
|
||||||
const router = useRouteSelector();
|
|
||||||
const [view, setView] = React.useState<View>(View.Text);
|
|
||||||
const [query] = useQuery({
|
|
||||||
query: CompareDocument,
|
|
||||||
variables: {
|
|
||||||
organization: router.organizationId,
|
|
||||||
project: router.projectId,
|
|
||||||
target: router.targetId,
|
|
||||||
version: router.versionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const canModifyState = useTargetAccess({
|
|
||||||
scope: TargetAccessScope.RegistryWrite,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const version = query.data?.schemaVersion;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataWrapper query={query}>
|
|
||||||
{() => (
|
|
||||||
<Page
|
|
||||||
title={`Schema Version`}
|
|
||||||
subtitle={version.id}
|
|
||||||
scrollable
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
{canModifyState && <MarkAsValid version={version} />}
|
|
||||||
<TextToggle
|
|
||||||
left={{
|
|
||||||
label: 'Changes',
|
|
||||||
value: View.Text,
|
|
||||||
}}
|
|
||||||
right={{
|
|
||||||
label: 'Diff View',
|
|
||||||
value: View.Diff,
|
|
||||||
}}
|
|
||||||
selected={view}
|
|
||||||
onSelect={setView}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div tw="h-full flex flex-col">
|
|
||||||
<div tw="mb-6 p-3 flex flex-row items-center space-x-12 bg-gray-100 dark:bg-gray-900 rounded-sm">
|
|
||||||
<div>
|
|
||||||
<Value title={version.date}>{formatDate(new Date(version.date), 'yyyy-MM-dd HH:mm')}</Value>
|
|
||||||
<ValueLabel>Published</ValueLabel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
label="Identifier of schema push action in your system, usually Git commit sha"
|
|
||||||
fontSize="xs"
|
|
||||||
placement="bottom-start"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Value>{version.commit.commit}</Value>
|
|
||||||
<ValueLabel>Commit</ValueLabel>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label="Author of the schema push action" fontSize="xs" placement="bottom-start">
|
|
||||||
<div>
|
|
||||||
<Value>{version.commit.author}</Value>
|
|
||||||
<ValueLabel>Author</ValueLabel>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{version.commit.service && (
|
|
||||||
<Tooltip label="Source of schema change" fontSize="xs" placement="bottom-start">
|
|
||||||
<div>
|
|
||||||
<Value>{version.commit.service}</Value>
|
|
||||||
<ValueLabel>Service</ValueLabel>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{version.valid && (
|
|
||||||
<Tooltip
|
|
||||||
label={version.valid ? 'Composed successfully' : 'Failed to compose'}
|
|
||||||
fontSize="xs"
|
|
||||||
placement="bottom-start"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Value>
|
|
||||||
<Status valid={version.valid} />
|
|
||||||
</Value>
|
|
||||||
<ValueLabel>Status</ValueLabel>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div tw="flex-grow">
|
|
||||||
<Compare view={view} comparison={query.data.schemaCompareToPrevious} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
)}
|
|
||||||
</DataWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TargetHistory() {
|
|
||||||
return <TargetView title="History">{({ organization }) => <VersionView organization={organization} />}</TargetView>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,465 +0,0 @@
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import tw from 'twin.macro';
|
|
||||||
import {
|
|
||||||
useDisclosure,
|
|
||||||
Modal,
|
|
||||||
Button,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalBody,
|
|
||||||
Code,
|
|
||||||
ModalFooter,
|
|
||||||
ModalCloseButton,
|
|
||||||
Alert,
|
|
||||||
AlertTitle,
|
|
||||||
AlertDescription,
|
|
||||||
Spinner,
|
|
||||||
Link,
|
|
||||||
InputGroup,
|
|
||||||
Input,
|
|
||||||
InputRightElement,
|
|
||||||
IconButton,
|
|
||||||
useColorModeValue,
|
|
||||||
Tooltip,
|
|
||||||
Editable,
|
|
||||||
EditablePreview,
|
|
||||||
EditableInput,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { VscPlug, VscClose, VscSync } from 'react-icons/vsc';
|
|
||||||
import { useQuery, useMutation } from 'urql';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
|
||||||
import {
|
|
||||||
SchemasDocument,
|
|
||||||
SchemasQuery,
|
|
||||||
ProjectFieldsFragment,
|
|
||||||
ProjectType,
|
|
||||||
TargetFieldsFragment,
|
|
||||||
OrganizationFieldsFragment,
|
|
||||||
CreateCdnTokenDocument,
|
|
||||||
SchemaSyncCdnDocument,
|
|
||||||
UpdateSchemaServiceNameDocument,
|
|
||||||
} from '@/graphql';
|
|
||||||
import { Description, Page } from '@/components/common';
|
|
||||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
|
||||||
import { GraphQLSDLBlock } from '@/components/common/GraphQLSDLBlock';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { NoSchemasYet } from '@/components/target/NoSchemasYet';
|
|
||||||
import { CopyValue } from '@/components/common/CopyValue';
|
|
||||||
import { useTargetAccess, TargetAccessScope } from '@/lib/access/target';
|
|
||||||
|
|
||||||
const Block = tw.div`mb-8`;
|
|
||||||
|
|
||||||
const SchemaServiceName: React.FC<{
|
|
||||||
version: string;
|
|
||||||
schema: SchemasQuery['target']['latestSchemaVersion']['schemas']['nodes'][0];
|
|
||||||
target: SchemasQuery['target'];
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ target, project, organization, schema, version }) => {
|
|
||||||
const [mutation, mutate] = useMutation(UpdateSchemaServiceNameDocument);
|
|
||||||
const hasAccess = useTargetAccess({
|
|
||||||
scope: TargetAccessScope.RegistryWrite,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submit = useCallback(
|
|
||||||
(newName: string) => {
|
|
||||||
if (schema.service === newName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newName.trim().length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate({
|
|
||||||
input: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
version,
|
|
||||||
name: schema.service!,
|
|
||||||
newName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[mutate]
|
|
||||||
);
|
|
||||||
|
|
||||||
if ((project.type !== ProjectType.Federation && project.type !== ProjectType.Stitching) || !hasAccess) {
|
|
||||||
return <>{schema.service}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Editable defaultValue={schema.service} isDisabled={mutation.fetching} onSubmit={submit}>
|
|
||||||
<EditablePreview />
|
|
||||||
<EditableInput />
|
|
||||||
</Editable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Schemas: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
target: SchemasQuery['target'];
|
|
||||||
filterService?: string;
|
|
||||||
}> = ({ organization, project, target, filterService }) => {
|
|
||||||
const schemas = target.latestSchemaVersion?.schemas.nodes ?? [];
|
|
||||||
|
|
||||||
if (!schemas.length) {
|
|
||||||
return <NoSchemasYet />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project.type === ProjectType.Single) {
|
|
||||||
return <GraphQLSDLBlock tw="mb-6" sdl={schemas[0].source} url={schemas[0].url} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{schemas
|
|
||||||
.filter(schema => {
|
|
||||||
if (filterService && schema.service) {
|
|
||||||
return schema.service.toLowerCase().includes(filterService.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map(schema => (
|
|
||||||
<Block key={schema.id}>
|
|
||||||
<GraphQLSDLBlock
|
|
||||||
sdl={schema.source}
|
|
||||||
url={schema.url}
|
|
||||||
title={
|
|
||||||
<SchemaServiceName
|
|
||||||
version={target.latestSchemaVersion?.id}
|
|
||||||
schema={schema}
|
|
||||||
target={target}
|
|
||||||
project={project}
|
|
||||||
organization={organization}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SchemaView: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
filterService?: string;
|
|
||||||
}> = ({ organization, project, target, filterService }) => {
|
|
||||||
const [query] = useQuery({
|
|
||||||
query: SchemasDocument,
|
|
||||||
variables: {
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
requestPolicy: 'cache-and-network',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataWrapper query={query}>
|
|
||||||
{() => (
|
|
||||||
<Schemas
|
|
||||||
organization={organization}
|
|
||||||
project={project}
|
|
||||||
target={query.data.target}
|
|
||||||
filterService={filterService}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DataWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectSchemaModal: React.FC<{
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
onClose(): void;
|
|
||||||
onOpen(): void;
|
|
||||||
isOpen: boolean;
|
|
||||||
}> = ({ target, project, organization, onClose, isOpen }) => {
|
|
||||||
const [generating, setGenerating] = React.useState(true);
|
|
||||||
const [mutation, mutate] = useMutation(CreateCdnTokenDocument);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isOpen) {
|
|
||||||
setGenerating(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutate({
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
},
|
|
||||||
}).then(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setGenerating(false);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
}, [isOpen, setGenerating, mutate]);
|
|
||||||
|
|
||||||
const description = `With high-availability and multi-zone CDN service based on
|
|
||||||
Cloudflare, Hive allows you to access ${
|
|
||||||
project.type === ProjectType.Federation
|
|
||||||
? 'the supergraph'
|
|
||||||
: project.type === ProjectType.Stitching
|
|
||||||
? 'the list of services'
|
|
||||||
: 'the schema'
|
|
||||||
} of your API,
|
|
||||||
through a secured external service, that's always up regardless of
|
|
||||||
Hive.`;
|
|
||||||
|
|
||||||
const generatingDescription = `Hive is now generating an authentication token and an URL you can use to fetch your ${
|
|
||||||
project.type === ProjectType.Federation
|
|
||||||
? 'supergraph schema'
|
|
||||||
: project.type === ProjectType.Stitching
|
|
||||||
? 'services'
|
|
||||||
: 'schema'
|
|
||||||
}.`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="3xl">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>Connect to Hive</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<Description>{description}</Description>
|
|
||||||
<div tw="pt-6">
|
|
||||||
{generating && (
|
|
||||||
<Alert
|
|
||||||
status="info"
|
|
||||||
variant="subtle"
|
|
||||||
flexDirection="column"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
textAlign="center"
|
|
||||||
height="200px"
|
|
||||||
>
|
|
||||||
<Spinner colorScheme="purple" />
|
|
||||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
|
||||||
Generating access...
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription maxWidth="sm">{generatingDescription}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!generating && mutation.data && (
|
|
||||||
<>
|
|
||||||
<Description tw="mb-6">You can use the following endpoint:</Description>
|
|
||||||
<CopyValue value={mutation.data.createCdnToken.url} width={'100%'} />
|
|
||||||
<Description tw="mt-6">To authenticate, use the following HTTP headers:</Description>
|
|
||||||
<Code tw="mt-6">X-Hive-CDN-Key: {mutation.data.createCdnToken.token}</Code>
|
|
||||||
{project.type === ProjectType.Federation && (
|
|
||||||
<Description tw="mt-6">
|
|
||||||
Read the{' '}
|
|
||||||
<Link
|
|
||||||
color="teal.500"
|
|
||||||
size="sm"
|
|
||||||
target="_blank"
|
|
||||||
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/registry-usage#apollo-federation`}
|
|
||||||
>
|
|
||||||
"Using the Registry with a Apollo Gateway"
|
|
||||||
</Link>{' '}
|
|
||||||
chapter in our documentation.
|
|
||||||
</Description>
|
|
||||||
)}
|
|
||||||
{project.type === ProjectType.Stitching && (
|
|
||||||
<Description tw="mt-6">
|
|
||||||
Read the{' '}
|
|
||||||
<Link
|
|
||||||
color="teal.500"
|
|
||||||
size="sm"
|
|
||||||
target="_blank"
|
|
||||||
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/registry-usage#schema-stitching`}
|
|
||||||
>
|
|
||||||
"Using the Registry when Stitching"
|
|
||||||
</Link>{' '}
|
|
||||||
chapter in our documentation.
|
|
||||||
</Description>
|
|
||||||
)}
|
|
||||||
{project.type === ProjectType.Single && (
|
|
||||||
<Description tw="mt-6">
|
|
||||||
Read the{' '}
|
|
||||||
<Link
|
|
||||||
color="teal.500"
|
|
||||||
size="sm"
|
|
||||||
target="_blank"
|
|
||||||
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/registry-usage#other-tools`}
|
|
||||||
>
|
|
||||||
"Using the Registry with any tool"
|
|
||||||
</Link>{' '}
|
|
||||||
chapter in our documentation.
|
|
||||||
</Description>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter tw="space-x-6">
|
|
||||||
<Button variant="ghost" type="button" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectSchemaButton: React.FC<{
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ target, project, organization }) => {
|
|
||||||
const { onClose, onOpen, isOpen } = useDisclosure();
|
|
||||||
const color = useColorModeValue('#fff', '#000');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button colorScheme="primary" type="button" size="sm" onClick={onOpen} leftIcon={<VscPlug color={color} />}>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
<ConnectSchemaModal
|
|
||||||
target={target}
|
|
||||||
project={project}
|
|
||||||
organization={organization}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
onOpen={onOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SyncSchemaButton: React.FC<{
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ target, project, organization }) => {
|
|
||||||
const color = useColorModeValue('#fff', '#000');
|
|
||||||
const [status, setStatus] = React.useState<'idle' | 'error' | 'success'>('idle');
|
|
||||||
const [mutation, mutate] = useMutation(SchemaSyncCdnDocument);
|
|
||||||
const hasAccess = useTargetAccess({
|
|
||||||
scope: TargetAccessScope.RegistryWrite,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sync = useCallback(() => {
|
|
||||||
mutate({
|
|
||||||
input: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
},
|
|
||||||
}).then(result => {
|
|
||||||
if (result.error) {
|
|
||||||
setStatus('error');
|
|
||||||
} else {
|
|
||||||
setStatus(result.data?.schemaSyncCDN.__typename === 'SchemaSyncCDNError' ? 'error' : 'success');
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
setStatus('idle');
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
}, [mutate, setStatus]);
|
|
||||||
|
|
||||||
if (!hasAccess || !target.hasSchema) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip label="Re-upload the latest valid version to Hive CDN" fontSize="xs" placement="bottom-start">
|
|
||||||
<Button
|
|
||||||
colorScheme={status === 'success' ? 'teal' : status === 'error' ? 'red' : 'primary'}
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
onClick={sync}
|
|
||||||
disabled={status !== 'idle' || mutation.fetching}
|
|
||||||
isLoading={mutation.fetching}
|
|
||||||
loadingText="Syncing..."
|
|
||||||
leftIcon={<VscSync color={color} />}
|
|
||||||
>
|
|
||||||
{status === 'idle' ? 'Update CDN' : status === 'error' ? 'Failed to synchronize' : 'CDN is up to date'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TargetSchemaInner({
|
|
||||||
organization,
|
|
||||||
project,
|
|
||||||
target,
|
|
||||||
}: {
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
}) {
|
|
||||||
const [filterService, setFilterService] = React.useState<string | null>(null);
|
|
||||||
const [term, setTerm] = React.useState<string | null>(null);
|
|
||||||
const debouncedFilter = useDebouncedCallback((value: string) => {
|
|
||||||
setFilterService(value);
|
|
||||||
}, 500);
|
|
||||||
const handleChange = React.useCallback(
|
|
||||||
event => {
|
|
||||||
debouncedFilter(event.target.value);
|
|
||||||
setTerm(event.target.value);
|
|
||||||
},
|
|
||||||
[debouncedFilter, setTerm]
|
|
||||||
);
|
|
||||||
const reset = React.useCallback(() => {
|
|
||||||
setFilterService('');
|
|
||||||
setTerm('');
|
|
||||||
}, [setFilterService]);
|
|
||||||
|
|
||||||
const isDistributed = project.type === ProjectType.Federation || project.type === ProjectType.Stitching;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
title="Schema"
|
|
||||||
subtitle="The latest schema you published for this target."
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
{isDistributed && (
|
|
||||||
<form
|
|
||||||
onSubmit={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InputGroup size="sm" variant="filled">
|
|
||||||
<Input type="text" placeholder="Find service" value={term} onChange={handleChange} />
|
|
||||||
<InputRightElement>
|
|
||||||
<IconButton aria-label="Reset" size="xs" variant="ghost" onClick={reset} icon={<VscClose />} />
|
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
<SyncSchemaButton target={target} project={project} organization={organization} />
|
|
||||||
<ConnectSchemaButton target={target} project={project} organization={organization} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SchemaView organization={organization} project={project} target={target} filterService={filterService} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TargetSchema() {
|
|
||||||
return (
|
|
||||||
<TargetView title="Overview">
|
|
||||||
{({ organization, project, target }) => (
|
|
||||||
<TargetSchemaInner organization={organization} project={project} target={target} />
|
|
||||||
)}
|
|
||||||
</TargetView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
/* eslint-disable import/no-extraneous-dependencies */
|
|
||||||
|
|
||||||
import 'graphiql/graphiql.css';
|
|
||||||
import 'twin.macro';
|
|
||||||
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { Button, useDisclosure, useColorModeValue } from '@chakra-ui/react';
|
|
||||||
import { VscPlug, VscSettings } from 'react-icons/vsc';
|
|
||||||
import { ConnectLabModal } from '@/components/lab/ConnectLabScreen';
|
|
||||||
import { CustomizeLabModal } from '@/components/lab/CustomizeLabScreen';
|
|
||||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
|
||||||
import React from 'react';
|
|
||||||
import type { GraphiQL as GraphiQLType } from 'graphiql';
|
|
||||||
import { Logo } from '@/components/common/Logo';
|
|
||||||
import { NoSchemasYet } from '@/components/target/NoSchemasYet';
|
|
||||||
|
|
||||||
const GraphiQL: typeof GraphiQLType = process.browser
|
|
||||||
? // eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
require('graphiql').default
|
|
||||||
: null;
|
|
||||||
|
|
||||||
export const ConnectLabTrigger: React.FC<{ endpoint: string }> = ({ endpoint }) => {
|
|
||||||
const { isOpen, onClose, onOpen: open } = useDisclosure();
|
|
||||||
const color = useColorModeValue('#fff', '#000');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button colorScheme="primary" type="button" size="sm" onClick={open} leftIcon={<VscPlug color={color} />}>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
<ConnectLabModal isOpen={isOpen} onClose={onClose} endpoint={endpoint} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: unused
|
|
||||||
export const CustomizeLabTrigger = () => {
|
|
||||||
const { isOpen, onClose, onOpen: open } = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button colorScheme="primary" type="button" onClick={open} leftIcon={<VscSettings color={'#ffffff'} />}>
|
|
||||||
Customize
|
|
||||||
</Button>
|
|
||||||
<CustomizeLabModal isOpen={isOpen} onClose={onClose} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SchemaLabContent: React.FC<{ endpoint: string }> = ({ endpoint }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div tw="h-full">
|
|
||||||
<GraphiQL
|
|
||||||
fetcher={createGraphiQLFetcher({
|
|
||||||
url: endpoint,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<GraphiQL.Logo>
|
|
||||||
<Logo tw="w-6 h-6" />
|
|
||||||
</GraphiQL.Logo>
|
|
||||||
</GraphiQL>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SchemaLabPage() {
|
|
||||||
return (
|
|
||||||
<TargetView title="Schema Laboratory">
|
|
||||||
{({ organization, project, target }) => {
|
|
||||||
const endpoint = `${window.location.origin}/api/lab/${organization.cleanId}/${project.cleanId}/${target.cleanId}`;
|
|
||||||
const noSchemas = target.latestSchemaVersion?.schemas.nodes?.length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
noPadding={true}
|
|
||||||
title="Schema Laboratory"
|
|
||||||
subtitle="Experiment, mock and create live environment for your schema, without running any backend."
|
|
||||||
actions={GraphiQL && !noSchemas ? <ConnectLabTrigger endpoint={endpoint} /> : null}
|
|
||||||
>
|
|
||||||
{GraphiQL ? noSchemas ? <NoSchemasYet /> : <SchemaLabContent endpoint={endpoint} /> : null}
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</TargetView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { useQuery } from 'urql';
|
|
||||||
|
|
||||||
import { Select, Stack } from '@chakra-ui/react';
|
|
||||||
import { VscChevronDown } from 'react-icons/vsc';
|
|
||||||
import {
|
|
||||||
HasCollectedOperationsDocument,
|
|
||||||
ProjectFieldsFragment,
|
|
||||||
TargetFieldsFragment,
|
|
||||||
OrganizationFieldsFragment,
|
|
||||||
} from '@/graphql';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { OperationsList } from '@/components/target/operations/List';
|
|
||||||
import { OperationsStats } from '@/components/target/operations/Stats';
|
|
||||||
import { EmptyList } from '@/components/common/EmptyList';
|
|
||||||
import { OperationsFilterTrigger } from '@/components/target/operations/Filters';
|
|
||||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
|
||||||
import { calculatePeriod, DATE_RANGE_OPTIONS, PeriodKey } from '@/components/common/TimeFilter';
|
|
||||||
|
|
||||||
const OperationsView: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
}> = ({ organization, project, target }) => {
|
|
||||||
const router = useRouteSelector();
|
|
||||||
const selectedPeriod: PeriodKey = (router.query.period as PeriodKey) ?? '1d';
|
|
||||||
const [selectedOperations, setSelectedOperations] = React.useState<string[]>([]);
|
|
||||||
|
|
||||||
const period = React.useMemo(() => calculatePeriod(selectedPeriod), [selectedPeriod]);
|
|
||||||
const updatePeriod = React.useCallback(
|
|
||||||
(ev: any) => {
|
|
||||||
router.update({ period: ev.target.value });
|
|
||||||
},
|
|
||||||
[router.update]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div tw="absolute top-7 right-4">
|
|
||||||
<Stack direction="row" spacing={4}>
|
|
||||||
<div>
|
|
||||||
<OperationsFilterTrigger period={period} selected={selectedOperations} onFilter={setSelectedOperations} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
variant="filled"
|
|
||||||
tw="cursor-pointer rounded-md"
|
|
||||||
defaultValue={selectedPeriod}
|
|
||||||
onChange={updatePeriod}
|
|
||||||
iconSize="16"
|
|
||||||
icon={<VscChevronDown />}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{DATE_RANGE_OPTIONS.map(item => {
|
|
||||||
return (
|
|
||||||
<option key={item.key} value={item.key}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<OperationsStats
|
|
||||||
organization={organization.cleanId}
|
|
||||||
project={project.cleanId}
|
|
||||||
target={target.cleanId}
|
|
||||||
period={period}
|
|
||||||
operationsFilter={selectedOperations}
|
|
||||||
/>
|
|
||||||
<OperationsList
|
|
||||||
tw="pt-12"
|
|
||||||
period={period}
|
|
||||||
organization={organization.cleanId}
|
|
||||||
project={project.cleanId}
|
|
||||||
target={target.cleanId}
|
|
||||||
operationsFilter={selectedOperations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OperationsViewGate: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
}> = ({ organization, project, target }) => {
|
|
||||||
const [query] = useQuery({
|
|
||||||
query: HasCollectedOperationsDocument,
|
|
||||||
variables: {
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataWrapper query={query}>
|
|
||||||
{result => {
|
|
||||||
if (!result.data.hasCollectedOperations) {
|
|
||||||
return (
|
|
||||||
<EmptyList
|
|
||||||
title="Hive is waiting for your first collected operation"
|
|
||||||
description="You can collect usage of your GraphQL API with Hive Client"
|
|
||||||
documentationLink={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/monitoring`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <OperationsView organization={organization} project={project} target={target} />;
|
|
||||||
}}
|
|
||||||
</DataWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TargetOperations() {
|
|
||||||
return (
|
|
||||||
<TargetView title="Operations">
|
|
||||||
{({ organization, project, target }) => (
|
|
||||||
<Page title="Operations" subtitle="Data collected based on operation executed against your GraphQL schema.">
|
|
||||||
<OperationsViewGate organization={organization} project={project} target={target} />
|
|
||||||
</Page>
|
|
||||||
)}
|
|
||||||
</TargetView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { useQuery } from 'urql';
|
|
||||||
import { Settings } from '@/components/common/Settings';
|
|
||||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
|
||||||
import { TargetView } from '@/components/target/View';
|
|
||||||
import { NameSettings } from '@/components/target/settings/Name';
|
|
||||||
import { DeleteSettings } from '@/components/target/settings/Delete';
|
|
||||||
import { TokensSettings } from '@/components/target/settings/Tokens';
|
|
||||||
import { ValidationSettings } from '@/components/target/settings/Validation';
|
|
||||||
import { BaseSchemaSettings } from '@/components/target/settings/BaseSchema';
|
|
||||||
import {
|
|
||||||
OrganizationFieldsFragment,
|
|
||||||
ProjectFieldsFragment,
|
|
||||||
TargetFieldsFragment,
|
|
||||||
TargetSettingsDocument,
|
|
||||||
} from '@/graphql';
|
|
||||||
import { useTargetAccess, TargetAccessScope } from '@/lib/access/target';
|
|
||||||
|
|
||||||
const Inner: React.FC<{
|
|
||||||
target: TargetFieldsFragment;
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ target, project, organization }) => {
|
|
||||||
const canAccess = useTargetAccess({
|
|
||||||
scope: TargetAccessScope.Settings,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
const canAccessTokens = useTargetAccess({
|
|
||||||
scope: TargetAccessScope.TokensRead,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
const [settings] = useQuery({
|
|
||||||
query: TargetSettingsDocument,
|
|
||||||
variables: {
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
target: target.cleanId,
|
|
||||||
},
|
|
||||||
targetsSelector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
project: project.cleanId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataWrapper query={settings}>
|
|
||||||
{() => (
|
|
||||||
<Settings title="Settings" subtitle="Tokens and stuff">
|
|
||||||
<NameSettings target={target} />
|
|
||||||
{canAccessTokens && <TokensSettings target={target} organization={organization} />}
|
|
||||||
<ValidationSettings
|
|
||||||
target={target}
|
|
||||||
possibleTargets={settings.data.targets.nodes}
|
|
||||||
settings={settings.data.targetSettings.validation}
|
|
||||||
/>
|
|
||||||
<BaseSchemaSettings target={target} />
|
|
||||||
<DeleteSettings />
|
|
||||||
</Settings>
|
|
||||||
)}
|
|
||||||
</DataWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TargetSettingsPage() {
|
|
||||||
return (
|
|
||||||
<TargetView title="Settings">
|
|
||||||
{({ target, project, organization }) => <Inner target={target} project={project} organization={organization} />}
|
|
||||||
</TargetView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { OrganizationFieldsFragment } from '@/graphql';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { ProjectView } from '@/components/project/View';
|
|
||||||
import { useProjectAccess, ProjectAccessScope } from '@/lib/access/project';
|
|
||||||
import { Alerts } from '@/components/project/alerts/Alerts';
|
|
||||||
import { Channels } from '@/components/project/alerts/Channels';
|
|
||||||
|
|
||||||
const Gate: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ organization }) => {
|
|
||||||
const canAccess = useProjectAccess({
|
|
||||||
scope: ProjectAccessScope.Alerts,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title="Alerts" subtitle="Be always up to date with all the updates">
|
|
||||||
<div tw="flex flex-col space-y-12 pt-6">
|
|
||||||
<Channels />
|
|
||||||
<Alerts />
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProjectSettingsPage() {
|
|
||||||
return <ProjectView title="Alerts">{({ organization }) => <Gate organization={organization} />}</ProjectView>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { OrganizationFieldsFragment, ProjectFieldsFragment } from '@/graphql';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { ProjectView } from '@/components/project/View';
|
|
||||||
import { ProjectTargets } from '@/components/project/Targets';
|
|
||||||
import { ProjectActivities } from '@/components/project/Activities';
|
|
||||||
import { TargetCreatorTrigger } from '@/components/target/Creator';
|
|
||||||
import { ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
|
|
||||||
|
|
||||||
const Inner: React.FC<{
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ project, organization }) => {
|
|
||||||
const canCreate = useProjectAccess({
|
|
||||||
scope: ProjectAccessScope.Read,
|
|
||||||
member: organization.me,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title={project.name} subtitle="An overview" actions={canCreate && <TargetCreatorTrigger />}>
|
|
||||||
<div tw="w-full flex flex-row">
|
|
||||||
<div tw="flex-grow mr-12">
|
|
||||||
<ProjectTargets project={project} organization={organization} />
|
|
||||||
</div>
|
|
||||||
<div tw="flex-grow-0 w-5/12">
|
|
||||||
<ProjectActivities />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProjectPage() {
|
|
||||||
return (
|
|
||||||
<ProjectView title="Overview">
|
|
||||||
{({ project, organization }) => <Inner project={project} organization={organization} />}
|
|
||||||
</ProjectView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { Dashboard } from '@/components/project/persisted-operations/Dashboard';
|
|
||||||
import { ProjectView } from '@/components/project/View';
|
|
||||||
import { ProjectFieldsFragment, OrganizationFieldsFragment } from '@/graphql';
|
|
||||||
import { ProjectAccessScope, useProjectAccess } from '@/lib/access/project';
|
|
||||||
|
|
||||||
const Inner: React.FC<{
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ project, organization }) => {
|
|
||||||
const canAccess = useProjectAccess({
|
|
||||||
scope: ProjectAccessScope.OperationsStoreRead,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
title="Operations Store"
|
|
||||||
subtitle="Operations you persisted using the Hive CLI. The store can be used to improve security and performance."
|
|
||||||
scrollable
|
|
||||||
>
|
|
||||||
<Dashboard project={project} organization={organization} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PersistedOperationsPage() {
|
|
||||||
return (
|
|
||||||
<ProjectView title="Persisted Operations">
|
|
||||||
{({ project, organization }) => <Inner project={project} organization={organization} />}
|
|
||||||
</ProjectView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { Settings } from '@/components/common/Settings';
|
|
||||||
import { ProjectView } from '@/components/project/View';
|
|
||||||
import { NameSettings } from '@/components/project/settings/Name';
|
|
||||||
import { DeleteSettings } from '@/components/project/settings/Delete';
|
|
||||||
import { GitRepositorySettings } from '@/components/project/settings/GitRepository';
|
|
||||||
import { ProjectFieldsFragment, OrganizationFieldsFragment } from '@/graphql';
|
|
||||||
import { useProjectAccess, ProjectAccessScope } from '@/lib/access/project';
|
|
||||||
|
|
||||||
const Inner: React.FC<{
|
|
||||||
project: ProjectFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ project, organization }) => {
|
|
||||||
const canAccess = useProjectAccess({
|
|
||||||
scope: ProjectAccessScope.Settings,
|
|
||||||
member: organization.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Settings title="Settings" subtitle="Applies to all targets within the project">
|
|
||||||
<NameSettings project={project} />
|
|
||||||
<GitRepositorySettings project={project} />
|
|
||||||
<DeleteSettings />
|
|
||||||
</Settings>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProjectSettingsPage() {
|
|
||||||
return (
|
|
||||||
<ProjectView title="Settings">
|
|
||||||
{({ project, organization }) => <Inner project={project} organization={organization} />}
|
|
||||||
</ProjectView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { OrganizationFieldsFragment } from '@/graphql';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { OrganizationView } from '@/components/organization/View';
|
|
||||||
import { OrganizationProjects } from '@/components/organization/Projects';
|
|
||||||
import { OrganizationActivities } from '@/components/organization/Activities';
|
|
||||||
import { ProjectCreatorTrigger } from '@/components/project/Creator';
|
|
||||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
|
||||||
|
|
||||||
const Inner: React.FC<{ organization: OrganizationFieldsFragment }> = ({ organization }) => {
|
|
||||||
const canCreate = useOrganizationAccess({
|
|
||||||
scope: OrganizationAccessScope.Read,
|
|
||||||
member: organization?.me,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title={organization.name} subtitle="An overview" actions={canCreate && <ProjectCreatorTrigger />}>
|
|
||||||
<div tw="w-full flex flex-row">
|
|
||||||
<div tw="flex-grow mr-12">
|
|
||||||
<OrganizationProjects org={organization} />
|
|
||||||
</div>
|
|
||||||
<div tw="flex-grow-0 w-5/12">
|
|
||||||
<OrganizationActivities />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OrganizationPage() {
|
|
||||||
return (
|
|
||||||
<OrganizationView title="Overview">{({ organization }) => <Inner organization={organization} />}</OrganizationView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { track } from '@/lib/mixpanel';
|
|
||||||
import { useQuery, useMutation } from 'urql';
|
|
||||||
import { Button, Checkbox, Table, Thead, Tbody, Tr, Th, Td, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { FaGoogle, FaGithub, FaKey } from 'react-icons/fa';
|
|
||||||
import { Page } from '@/components/common';
|
|
||||||
import { CopyValue } from '@/components/common/CopyValue';
|
|
||||||
import { OrganizationView } from '@/components/organization/View';
|
|
||||||
import { MemberPermisssonsModal } from '@/components/organization/members/PermissionsModal';
|
|
||||||
import {
|
|
||||||
OrganizationFieldsFragment,
|
|
||||||
OrganizationMembersDocument,
|
|
||||||
ResetInviteCodeDocument,
|
|
||||||
OrganizationMembersQuery,
|
|
||||||
DeleteOrganizationMembersDocument,
|
|
||||||
MemberFieldsFragment,
|
|
||||||
AuthProvider,
|
|
||||||
} from '@/graphql';
|
|
||||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
|
||||||
import { useNotifications } from '@/lib/hooks/use-notifications';
|
|
||||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
|
||||||
import { DataWrapper } from '@/components/common/DataWrapper';
|
|
||||||
|
|
||||||
const Invitation: React.FC<{
|
|
||||||
organization: OrganizationMembersQuery['organization']['organization'];
|
|
||||||
}> = ({ organization }) => {
|
|
||||||
const inviteUrl = `${window.location.origin}/join/${organization.inviteCode}`;
|
|
||||||
const router = useRouteSelector();
|
|
||||||
const notify = useNotifications();
|
|
||||||
const [mutation, mutate] = useMutation(ResetInviteCodeDocument);
|
|
||||||
|
|
||||||
const generate = React.useCallback(() => {
|
|
||||||
track('GENERATE_NEW_INVITATION_LINK_ATTEMPT', {
|
|
||||||
organization: router.organizationId,
|
|
||||||
});
|
|
||||||
mutate({
|
|
||||||
selector: {
|
|
||||||
organization: router.organizationId,
|
|
||||||
},
|
|
||||||
}).finally(() => {
|
|
||||||
notify('Generated new invitation link', 'info');
|
|
||||||
});
|
|
||||||
}, [mutate, notify]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div tw="flex flex-row space-x-3 pb-3">
|
|
||||||
<CopyValue value={inviteUrl} />
|
|
||||||
<Button type="button" onClick={generate} disabled={mutation.fetching}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemberRow: React.FC<{
|
|
||||||
member: MemberFieldsFragment;
|
|
||||||
owner: MemberFieldsFragment;
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
checked: string[];
|
|
||||||
onCheck(id: string): void;
|
|
||||||
}> = ({ member, owner, checked, onCheck, organization }) => {
|
|
||||||
const isOwner = member.id === owner.id;
|
|
||||||
const isMe = member.id === organization.me.id;
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
|
|
||||||
const canManage = !isOwner && !isMe;
|
|
||||||
|
|
||||||
const provider = member.user.provider;
|
|
||||||
const providerIcon =
|
|
||||||
provider === AuthProvider.Google ? (
|
|
||||||
<FaGoogle color="#34a853" />
|
|
||||||
) : provider === AuthProvider.Github ? (
|
|
||||||
<FaGithub color="#333" />
|
|
||||||
) : (
|
|
||||||
<FaKey color="#fbbc05" />
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MemberPermisssonsModal isOpen={isOpen} onClose={onClose} member={member} organization={organization} />
|
|
||||||
<Tr>
|
|
||||||
<Td>
|
|
||||||
<Checkbox
|
|
||||||
colorScheme="primary"
|
|
||||||
isDisabled={!canManage}
|
|
||||||
checked={checked.includes(member.id)}
|
|
||||||
onChange={() => onCheck(member.id)}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td textAlign="center">{providerIcon}</Td>
|
|
||||||
<Td>{member.user.displayName}</Td>
|
|
||||||
<Td>{member.user.email}</Td>
|
|
||||||
<Td textAlign="right">
|
|
||||||
{canManage && (
|
|
||||||
<Button size="sm" variant="ghost" onClick={onOpen}>
|
|
||||||
Change permissions
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MembersManager: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment;
|
|
||||||
}> = ({ organization }) => {
|
|
||||||
const [query] = useQuery({
|
|
||||||
query: OrganizationMembersDocument,
|
|
||||||
variables: {
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [checked, setChecked] = React.useState<string[]>([]);
|
|
||||||
const onCheck = React.useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
if (checked.includes(id)) {
|
|
||||||
setChecked(checked.filter(i => i !== id));
|
|
||||||
} else {
|
|
||||||
setChecked(checked.concat(id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[checked, setChecked]
|
|
||||||
);
|
|
||||||
const [mutation, mutate] = useMutation(DeleteOrganizationMembersDocument);
|
|
||||||
const deleteMembers = React.useCallback(() => {
|
|
||||||
mutate({
|
|
||||||
selector: {
|
|
||||||
organization: organization.cleanId,
|
|
||||||
users: checked,
|
|
||||||
},
|
|
||||||
}).finally(() => {
|
|
||||||
setChecked([]);
|
|
||||||
});
|
|
||||||
}, [mutate, checked, setChecked]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataWrapper query={query}>
|
|
||||||
{() => (
|
|
||||||
<div>
|
|
||||||
<div tw="flex flex-row justify-between pb-3">
|
|
||||||
<Invitation organization={query.data.organization.organization} />
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
disabled={!checked.length || mutation.fetching}
|
|
||||||
onClick={deleteMembers}
|
|
||||||
type="button"
|
|
||||||
colorScheme="red"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th tw="w-10"></Th>
|
|
||||||
<Th tw="w-10"></Th>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>Email</Th>
|
|
||||||
<Th textAlign="right">Permissions</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{query.data.organization.organization?.members.nodes.map(member => (
|
|
||||||
<MemberRow
|
|
||||||
key={member.id}
|
|
||||||
member={member}
|
|
||||||
organization={query.data.organization.organization}
|
|
||||||
owner={query.data.organization.organization.owner}
|
|
||||||
checked={checked}
|
|
||||||
onCheck={onCheck}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DataWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Inner: React.FC<{ organization: OrganizationFieldsFragment }> = ({ organization }) => {
|
|
||||||
const canAccess = useOrganizationAccess({
|
|
||||||
scope: OrganizationAccessScope.Members,
|
|
||||||
member: organization?.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page title="Members" subtitle="Invite others to your organization and manage access.">
|
|
||||||
<MembersManager organization={organization} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OrganizationSettingsPage() {
|
|
||||||
return (
|
|
||||||
<OrganizationView title="Members">{({ organization }) => <Inner organization={organization} />}</OrganizationView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
|
||||||
import { Settings } from '@/components/common/Settings';
|
|
||||||
import { OrganizationView } from '@/components/organization/View';
|
|
||||||
import { DeleteSettings } from '@/components/organization/settings/Delete';
|
|
||||||
import { NameSettings } from '@/components/organization/settings/Name';
|
|
||||||
import { IntegrationsSettings } from '@/components/organization/settings/Integrations';
|
|
||||||
import { OrganizationFieldsFragment, OrganizationType } from '@/graphql';
|
|
||||||
|
|
||||||
const Inner: React.FC<{ organization: OrganizationFieldsFragment }> = ({ organization }) => {
|
|
||||||
const canAccess = useOrganizationAccess({
|
|
||||||
scope: OrganizationAccessScope.Settings,
|
|
||||||
member: organization?.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
const canAccessIntegrations = useOrganizationAccess({
|
|
||||||
scope: OrganizationAccessScope.Integrations,
|
|
||||||
member: organization?.me,
|
|
||||||
redirect: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRegular = organization.type === OrganizationType.Regular;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Settings title="Settings" subtitle="Applies to all projects and targets within the organization">
|
|
||||||
{isRegular && <NameSettings organization={organization} />}
|
|
||||||
{canAccessIntegrations && <IntegrationsSettings organization={organization} />}
|
|
||||||
{isRegular && <DeleteSettings />}
|
|
||||||
</Settings>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OrganizationSettingsPage() {
|
|
||||||
return (
|
|
||||||
<OrganizationView title="Settings">{({ organization }) => <Inner organization={organization} />}</OrganizationView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import 'twin.macro';
|
|
||||||
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
|
|
||||||
import { OrganizationView } from '@/components/organization/View';
|
|
||||||
import { OrganizationUsageEstimationView } from '@/components/organization/Usage';
|
|
||||||
import { OrganizationFieldsFragment, OrgBillingInfoFieldsFragment, OrgRateLimitFieldsFragment } from '@/graphql';
|
|
||||||
import { Card, Page } from '@/components/common';
|
|
||||||
import { BillingView } from '@/components/organization/billing/Billing';
|
|
||||||
import { Button, Stat, StatHelpText, StatLabel, StatNumber } from '@chakra-ui/react';
|
|
||||||
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
|
|
||||||
import { InvoicesList } from '@/components/organization/billing/InvoicesList';
|
|
||||||
import { CurrencyFormatter } from '@/components/organization/billing/helpers';
|
|
||||||
import { RateLimitWarn } from '@/components/organization/billing/RateLimitWarn';
|
|
||||||
|
|
||||||
const Inner: React.FC<{
|
|
||||||
organization: OrganizationFieldsFragment & OrgBillingInfoFieldsFragment & OrgRateLimitFieldsFragment;
|
|
||||||
}> = ({ organization }) => {
|
|
||||||
const router = useRouteSelector();
|
|
||||||
const canAccess = useOrganizationAccess({
|
|
||||||
scope: OrganizationAccessScope.Settings,
|
|
||||||
member: organization?.me,
|
|
||||||
redirect: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canAccess) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
title={'Subscription'}
|
|
||||||
subtitle={'Information about your Hive plan, subscription, usage and data ingestion.'}
|
|
||||||
actions={
|
|
||||||
<Button
|
|
||||||
colorScheme="primary"
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
as="a"
|
|
||||||
href={`/${router.organizationId}/subscription/manage`}
|
|
||||||
>
|
|
||||||
Manage Subscription
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RateLimitWarn organization={organization} />
|
|
||||||
<div tw="w-full flex flex-row">
|
|
||||||
<div tw="flex-grow mr-12">
|
|
||||||
<div tw="flex flex-col space-y-6 pb-6">
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Title>Plan and Reserved Volume</Card.Title>
|
|
||||||
<Card.Content>
|
|
||||||
<BillingView organization={organization}>
|
|
||||||
{organization.billingConfiguration?.upcomingInvoice ? (
|
|
||||||
<Stat tw="mb-4">
|
|
||||||
<StatLabel>Next Invoice</StatLabel>
|
|
||||||
<StatNumber>
|
|
||||||
{CurrencyFormatter.format(organization.billingConfiguration.upcomingInvoice.amount)}
|
|
||||||
</StatNumber>
|
|
||||||
<StatHelpText>{organization.billingConfiguration.upcomingInvoice.date}</StatHelpText>
|
|
||||||
</Stat>
|
|
||||||
) : null}
|
|
||||||
</BillingView>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div tw="flex-grow-0 w-5/12">
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Title>Monthly Usage Overview</Card.Title>
|
|
||||||
<Card.Content>
|
|
||||||
<OrganizationUsageEstimationView organization={organization} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{organization.billingConfiguration?.invoices?.length > 0 ? (
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Title>Invoices</Card.Title>
|
|
||||||
<Card.Content>
|
|
||||||
<InvoicesList organization={organization} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
) : null}
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SubscriptionPage() {
|
|
||||||
return (
|
|
||||||
<OrganizationView title="Subscription & Usage" includeBilling={true} includeRateLimit={true}>
|
|
||||||
{({ organization }) => <Inner organization={organization} />}
|
|
||||||
</OrganizationView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue