feat: remove the legacy routes (#81)

This commit is contained in:
Laurin Quast 2022-05-25 12:13:18 +02:00 committed by GitHub
parent 4e23275fab
commit 5af93b885e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 0 additions and 1460 deletions

View file

@ -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>;
}

View file

@ -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>;
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>;
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}