mirror of
https://github.com/graphql-hive/console
synced 2026-05-22 00:28:46 +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