feat: remove app next.js built-time environment variable requirement (#382)

* feat: retreive stripe public key via runtime environment variable instead of a build-time environment variable

* feat: retreive google and github enabled environment variable  via runtime environment variable instead of a build-time environment variable

* feat: retreive app base url  environment variable via runtime environment variable instead of a build-time environment variable

* feat: retreive mixpanel token environment variable via runtime environment variable instead of a build-time environment variable

* lazy initialize app info

* feat: provide ga tarcking id and crisp website id via environment variable

* make docs url optional

* feat: load sentry config from environment variables

* document hive app environment variables

* add application dockerfile

* add docker build instructions

* lul

* set working directory

* pls fix

* lol this is apollo-router not graphql-hive

* feat: only show sentry stuff if sentry environment variables are set

* use LTS node version

* No mixpanel

* Fallback

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
This commit is contained in:
Laurin Quast 2022-09-21 12:47:40 +02:00 committed by GitHub
parent 2931441473
commit a03cc58e5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 380 additions and 125 deletions

View file

@ -111,7 +111,7 @@ jobs:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
IMAGE_NAME: ${{ github.repository }}/apollo-router
permissions:
contents: read

View file

@ -109,8 +109,6 @@ jobs:
- name: Build
run: yarn workspace integration-tests run build-and-pack
env:
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }}
- name: Pull images
run: docker-compose -f integration-tests/docker-compose.yml pull
@ -119,6 +117,7 @@ jobs:
run: yarn workspace integration-tests run dockest
timeout-minutes: 15
env:
STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }}
SUPERTOKENS_CONNECTION_URI: http://127.0.0.1:3567
SUPERTOKENS_API_KEY: bubatzbieber6942096420
@ -177,8 +176,6 @@ jobs:
- name: Build
if: steps.feature_flags.outputs.detected == 'true'
run: yarn workspace integration-tests run build-and-pack
env:
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }}
- name: Pull images
if: steps.feature_flags.outputs.detected == 'true'
@ -189,6 +186,7 @@ jobs:
run: yarn workspace integration-tests run dockest
timeout-minutes: 15
env:
STRIPE_PUBLIC_KEY: ${{ secrets.TEST_STRIPE_PUBLIC_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.TEST_STRIPE_SECRET_KEY }}
SUPERTOKENS_CONNECTION_URI: http://127.0.0.1:3567
SUPERTOKENS_API_KEY: bubatzbieber6942096420
@ -404,3 +402,59 @@ jobs:
- name: Lint Prettier
run: yarn lint:prettier
publish_app:
needs: setup
name: Publish for Docker
runs-on: ubuntu-latest
timeout-minutes: 40
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v2
with:
node-version-file: .node-version
- name: Pull node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ github.sha }}
- name: Setup Turbo
run: node ./scripts/turborepo-setup.js
- name: Generate Types
run: yarn graphql:generate
- name: Build
run: yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
file: packages/web/app/Dockerfile
context: ./packages/web/app
push: true
tags: ${{ env.REGISTRY }}/${{ github.repository }}/app:${{ github.sha }}

View file

@ -62,28 +62,22 @@ export function deployApp({
env: [
{ name: 'DEPLOYED_DNS', value: deploymentEnv.DEPLOYED_DNS },
{ name: 'NODE_ENV', value: 'production' },
{ name: 'ENVIRONMENT', value: deploymentEnv.ENVIRONMENT },
{
name: 'NEXT_PUBLIC_ENVIRONMENT',
name: 'ENVIRONMENT',
value: deploymentEnv.ENVIRONMENT,
},
{
name: 'RELEASE',
value: appRelease,
},
{
name: 'NEXT_PUBLIC_RELEASE',
value: appRelease,
},
{ name: 'SENTRY_DSN', value: commonEnv.SENTRY_DSN },
{ name: 'NEXT_PUBLIC_SENTRY_DSN', value: commonEnv.SENTRY_DSN },
{ name: 'SENTRY_ENABLED', value: commonEnv.SENTRY_ENABLED },
{
name: 'GRAPHQL_ENDPOINT',
value: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`),
},
{
name: 'NEXT_PUBLIC_APP_BASE_URL',
name: 'APP_BASE_URL',
value: `https://${deploymentEnv.DEPLOYED_DNS}/`,
},
{
@ -99,6 +93,26 @@ export function deployApp({
value: githubAppConfig.require('name'),
},
{
name: 'STRIPE_PUBLIC_KEY',
value: appEnv.STRIPE_PUBLIC_KEY,
},
{
name: 'GA_TRACKING_ID',
value: appEnv.GA_TRACKING_ID,
},
{
name: 'CRISP_WEBSITE_ID',
value: appEnv.CRISP_WEBSITE_ID,
},
{
name: 'DOCS_URL',
value: appEnv.DOCS_URL,
},
//
// AUTH
//
@ -146,7 +160,7 @@ export function deployApp({
},
// GitHub
{
name: 'NEXT_PUBLIC_AUTH_GITHUB',
name: 'AUTH_GITHUB',
value: '1',
},
{
@ -159,7 +173,7 @@ export function deployApp({
},
// Google
{
name: 'NEXT_PUBLIC_AUTH_GOOGLE',
name: 'AUTH_GOOGLE',
value: '1',
},
{

View file

@ -1,6 +1,5 @@
GRAPHQL_ENDPOINT="http://localhost:4000/graphql"
APP_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_APP_BASE_URL="http://localhost:3000"
# Supertokens
@ -10,12 +9,12 @@ SUPERTOKENS_API_KEY="bubatzbieber6942096420"
# Auth Provider
## Enable GitHub login
NEXT_PUBLIC_AUTH_GITHUB=1
AUTH_GITHUB=1
AUTH_GITHUB_CLIENT_ID="<sync>"
AUTH_GITHUB_CLIENT_SECRET="<sync>"
## Enable Google login
NEXT_PUBLIC_AUTH_GOOGLE=1
AUTH_GOOGLE=1
AUTH_GOOGLE_CLIENT_ID="<sync>"
AUTH_GOOGLE_CLIENT_SECRET="<sync>"
@ -38,7 +37,7 @@ SLACK_CLIENT_SECRET="<sync>"
GITHUB_APP_NAME="<sync>"
# Stripe
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="<sync>"
STRIPE_PUBLIC_KEY="<sync>"
# Emails Service

View file

@ -0,0 +1,24 @@
FROM node:16-slim as install
WORKDIR /usr/src/app
COPY dist/ /usr/src/app/
ENV NODE_ENV production
# DANGER: there is no lockfile :)
# in the future this should be improved...
RUN npm install --legacy-peer-deps
FROM node:16-slim as app
WORKDIR /usr/src/app
COPY --from=install /usr/src/app/ /usr/src/app/
ENV ENVIRONMENT production
ENV RELEASE ${RELEASE}
ENV PORT 3000
CMD ["node", "index.js"]

View file

@ -0,0 +1,63 @@
# `@hive/app`
The Hive application as seen on https://app.graphql-hive.com/.
## Configuration
The following environment variables configure the application.
| Name | Required | Description | Example Value |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `APP_BASE_URL` | **Yes** | The base url of the app, | `https://app.graphql-hive.com` |
| `GRAPHQL_ENDPOINT` | **Yes** | The endpoint of the Hive GraphQL API. | `http://127.0.0.1:4000/graphql` |
| `EMAILS_ENDPOINT` | **Yes** | The endpoint of the GraphQL Hive Email service. | `http://127.0.0.1:6260` |
| `SUPERTOKENS_CONNECTION_URI` | **Yes** | The URI of the SuperTokens instance. | `http://127.0.0.1:3567` |
| `SUPERTOKENS_API_KEY` | **Yes** | The API KEY of the SuperTokens instance. | `iliketurtlesandicannotlie` |
| `SLACK_CLIENT_ID` | **Yes** | The Slack client ID. | `g6aff8102efda5e1d12e` |
| `SLACK_CLIENT_SECRET` | **Yes** | The Slack client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `GITHUB_APP_NAME` | **Yes** | The GitHub App name. | `graphql-hive-self-hosted` |
| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) |
| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` |
| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `AUTH_GOOGLE` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) |
| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` |
| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` |
| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` |
| `SENTRY_ENABLED` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) |
| `DOCS_URL` | No | The URL of the Hive Docs | `https://docs.graphql-hive.com` |
| `NODE_ENV` | No | The `NODE_ENV` value. | `production` |
| `GA_TRACKING_ID` | No | The token for Google Analytics in order to track user actions. | `g6aff8102efda5e1d12e` |
| `CRISP_WEBSITE_ID` | No | The Crisp Website ID | `g6aff8102efda5e1d12e` |
## Hive Hosted Configuration
This is only important if you are hosting Hive for getting 💰.
### Payments
| Name | Required | Description | Example Value |
| ------------------- | -------- | ---------------------- | ---------------------- |
| `STRIPE_PUBLIC_KEY` | No | The Stripe Public Key. | `g6aff8102efda5e1d12e` |
### Legacy Auth0 Configuration
If you are not self-hosting GraphQL Hive, you can ignore this section. It is only required for the legacy Auth0 compatibility layer.
| Name | Required | Description | Example Value |
| ----------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `AUTH_LEGACY_AUTH0` | No | Whether the legacy Auth0 import is enabled. | `1` (enabled) or `0` (disabled) |
| `AUTH_LEGACY_AUTH0_CLIENT_ID` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 client ID. | `rDSpExxD8sfqlpF1kbxxLkMNYI2Sxxx` |
| `AUTH_LEGACY_AUTH0_CLIENT_SECRET` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 client secret. | `e43f156xx54en2b56117dc4abc4491dxxbb6b187` |
| `AUTH_LEGACY_AUTH0_ISSUER_BASE_URL` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 issuer base url. | `https://your-project.us.auth0.com` |
| `AUTH_LEGACY_AUTH0_AUDIENCE` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The Auth0 audience | `https://your-project.us.auth0.com/api/v2/` |
| `AUTH_LEGACY_AUTH0_INTERNAL_API_ENDPOINT` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The internal endpoint for importing Auth0 accounts. (**Note:** This route is within the GraphQL service.) | `http://127.0.0.1:4000/__legacy` |
| `AUTH_LEGACY_AUTH0_INTERNAL_API_KEY` | No (**Yes** if `AUTH_LEGACY_AUTH0` is set) | The internal endpoint key. | `iliketurtles` |
### Building the Docker Image
**Prerequisites:** Make sure you built the mono-repository using `yarn build`.
```bash
docker build . --build-arg RELEASE=stable-main -t graphql-hive/app
```

View file

@ -10,9 +10,26 @@ declare module '@n1ru4l/react-time-ago' {
declare namespace NodeJS {
export interface ProcessEnv {
NEXT_PUBLIC_APP_BASE_URL: string;
APP_BASE_URL: string;
GITHUB_APP_NAME: string;
GRAPHQL_ENDPOINT: string;
SUPERTOKENS_CONNECTION_URI: string;
}
}
// eslint-disable-next-line no-var
declare var __ENV__:
| undefined
| {
APP_BASE_URL: string;
DOCS_URL: string | undefined;
STRIPE_PUBLIC_KEY: string | undefined;
AUTH_GITHUB: string | undefined;
AUTH_GOOGLE: string | undefined;
GA_TRACKING_ID: string | undefined;
CRISP_WEBSITE_ID: string | undefined;
SENTRY_DSN: string | undefined;
RELEASE: string | undefined;
ENVIRONMENT: string | undefined;
SENTRY_ENABLED: string | undefined;
};

View file

@ -18,6 +18,7 @@ import {
ProjectFieldsFragment,
TargetFieldsFragment,
} from '@/graphql';
import { getDocsUrl } from '@/lib/docs-url';
function floorDate(date: Date): Date {
const time = 1000 * 60;
@ -144,7 +145,7 @@ const OperationsViewGate = ({
<EmptyList
title="Hive is waiting for your first collected operation"
description="You can collect usage of your GraphQL API with Hive Client"
docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/monitoring`}
docsUrl={getDocsUrl(`/features/monitoring`)}
/>
)
}

View file

@ -8,6 +8,7 @@ import { ProjectLayout } from '@/components/layouts';
import { Activities, Badge, Button, Card, DropdownMenu, EmptyList, Heading, TimeAgo, Title } from '@/components/v2';
import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon';
import { TargetQuery, TargetsDocument, VersionsDocument } from '@/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { useClipboard } from '@/lib/hooks/use-clipboard';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
@ -109,7 +110,7 @@ const Page = () => {
<EmptyList
title="Hive is waiting for your first target"
description='You can create a target by clicking the "New Target" button'
docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/targets`}
docsUrl={getDocsUrl(`/get-started/targets`)}
/>
) : (
targets?.nodes.map(target => <TargetCard key={target.id} target={target} />)

View file

@ -1,4 +1,4 @@
import { ReactElement } from 'react';
import React, { ReactElement } from 'react';
import NextLink from 'next/link';
import { onlyText } from 'react-children-utilities';
import { useQuery } from 'urql';
@ -9,6 +9,7 @@ import { Activities, Button, Card, DropdownMenu, EmptyList, Heading, Skeleton, T
import { getActivity } from '@/components/v2/activities';
import { LinkIcon, MoreIcon, SettingsIcon } from '@/components/v2/icon';
import { ProjectActivitiesDocument, ProjectsWithTargetsDocument, ProjectsWithTargetsQuery } from '@/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { fixDuplicatedFragments } from '@/lib/graphql';
import { useClipboard } from '@/lib/hooks/use-clipboard';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
@ -94,6 +95,7 @@ function ProjectsPage(): ReactElement {
},
},
});
return (
<>
<Title title="Projects" />
@ -106,7 +108,7 @@ function ProjectsPage(): ReactElement {
<EmptyList
title="Hive is waiting for your first project"
description='You can create a project by clicking the "Create Project" button'
docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/projects`}
docsUrl={getDocsUrl(`/get-started/projects`)}
/>
) : (
<div className="grid grid-cols-2 gap-5">

View file

@ -13,6 +13,7 @@ import { OrganizationUsageEstimationView } from '@/components/organization/Usage
import { Card, Heading, Tabs, Title } from '@/components/v2';
import { OrganizationFieldsFragment, OrgBillingInfoFieldsFragment, OrgRateLimitFieldsFragment } from '@/graphql';
import { OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
const DateFormatter = Intl.DateTimeFormat('en-US', {
month: 'short',
@ -110,6 +111,22 @@ function SubscriptionPage(): ReactElement {
);
}
export const getServerSideProps = withSessionProtection();
export const getServerSideProps = withSessionProtection(async context => {
/**
* If Strive is not enabled we redirect the user to the organization.
*/
const isStripeEnabled = getIsStripeEnabled();
if (isStripeEnabled === false) {
const parts = `${context.resolvedUrl}`.split('/');
parts.pop();
return {
redirect: {
destination: `${parts.join('/')}`,
permanent: false,
},
};
}
return { props: {} };
});
export default authenticated(SubscriptionPage);

View file

@ -2,16 +2,50 @@ import 'regenerator-runtime/runtime';
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document';
import { extractCritical } from '@emotion/server';
export default class MyDocument extends Document {
type FrontendEnvironment = {
APP_BASE_URL: string | undefined;
DOCS_URL: string | undefined;
STRIPE_PUBLIC_KEY: string | undefined;
AUTH_GITHUB: string | undefined;
AUTH_GOOGLE: string | undefined;
GA_TRACKING_ID: string | undefined;
CRISP_WEBSITE_ID: string | undefined;
SENTRY_DSN: string | undefined;
RELEASE: string | undefined;
ENVIRONMENT: string | undefined;
SENTRY_ENABLED: string | undefined;
};
export default class MyDocument extends Document<{ ids: Array<string>; css: string; __ENV__: FrontendEnvironment }> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const page = await ctx.renderPage();
const styles = extractCritical(page.html);
return { ...initialProps, ...page, ...styles };
const __ENV__: FrontendEnvironment = {
APP_BASE_URL: process.env['APP_BASE_URL'],
DOCS_URL: process.env['DOCS_URL'],
STRIPE_PUBLIC_KEY: process.env['STRIPE_PUBLIC_KEY'],
AUTH_GITHUB: process.env['AUTH_GITHUB'],
AUTH_GOOGLE: process.env['AUTH_GOOGLE'],
GA_TRACKING_ID: process.env['GA_TRACKING_ID'],
CRISP_WEBSITE_ID: process.env['CRISP_WEBSITE_ID'],
SENTRY_DSN: process.env['SENTRY_DSN'],
RELEASE: process.env['RELEASE'],
ENVIRONMENT: process.env['ENVIRONMENT'],
SENTRY_ENABLED: process.env['SENTRY_ENABLED'],
};
return {
...initialProps,
...page,
...styles,
__ENV__,
};
}
render() {
const { ids, css } = this.props as any;
const { ids, css } = this.props;
return (
<Html className="dark">
@ -36,6 +70,12 @@ export default class MyDocument extends Document {
id="force-dark-mode"
dangerouslySetInnerHTML={{ __html: "localStorage['chakra-ui-color-mode'] = 'dark';" }}
/>
<script
type="module"
dangerouslySetInnerHTML={{
__html: `globalThis["__ENV__"] = ${JSON.stringify((this.props as any).__ENV__)}`,
}}
/>
</Head>
<body className="bg-transparent font-sans text-white">
<Main />

View file

@ -15,7 +15,7 @@ export default async function superTokens(req: NextApiRequest & Request, res: Ne
// NOTE: We need CORS only if we are querying the APIs from a different origin
await NextCors(req, res, {
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
origin: process.env['NEXT_PUBLIC_APP_BASE_URL'],
origin: process.env['APP_BASE_URL'],
credentials: true,
allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()],
});

View file

@ -10,7 +10,7 @@ export async function ensureGithubIntegration(
) {
const { orgId, installationId } = input;
await graphql({
url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
headers: {
...req.headers,
'content-type': 'application/json',

View file

@ -7,7 +7,7 @@ export default async function githubConnectOrg(req: NextApiRequest, res: NextApi
const url = `https://github.com/apps/${process.env.GITHUB_APP_NAME}/installations/new`;
const redirectUrl = `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/github/callback`;
const redirectUrl = `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/github/callback`;
res.redirect(`${url}?state=${orgId}&redirect_url=${redirectUrl}`);
}

View file

@ -18,7 +18,7 @@ export default async function githubSetupCallback(req: NextApiRequest, res: Next
cleanId: string;
};
}>({
url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
headers: {
...req.headers,
'content-type': 'application/json',

View file

@ -32,7 +32,7 @@ export default async function slackCallback(req: NextApiRequest, res: NextApiRes
const token = slackResponse.access_token;
await graphql({
url: `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
url: `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/proxy`,
headers: {
...req.headers,
'content-type': 'application/json',

View file

@ -7,7 +7,7 @@ export default async function slackConnectOrg(req: NextApiRequest, res: NextApiR
const slackUrl = `https://slack.com/oauth/v2/authorize?scope=incoming-webhook,chat:write,chat:write.public,commands&client_id=${process
.env.SLACK_CLIENT_ID!}`;
const redirectUrl = `${process.env['NEXT_PUBLIC_APP_BASE_URL'].replace(/\/$/, '')}/api/slack/callback`;
const redirectUrl = `${process.env['APP_BASE_URL'].replace(/\/$/, '')}/api/slack/callback`;
res.redirect(`${slackUrl}&state=${orgId}&redirect_uri=${redirectUrl}`);
}

View file

@ -1,14 +1,14 @@
import { init } from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const RELEASE = process.env.RELEASE || process.env.NEXT_PUBLIC_RELEASE;
const ENVIRONMENT = process.env.ENVIRONMENT || process.env.NEXT_PUBLIC_ENVIRONMENT;
const SENTRY_ENABLED = process.env.SENTRY_ENABLED || process.env.NEXT_PUBLIC_SENTRY_ENABLED;
const SENTRY_DSN = globalThis.process?.env['SENTRY_DSN'] ?? globalThis['__ENV__']?.['SENTRY_DSN'];
const RELEASE = globalThis.process?.env['RELEASE'] ?? globalThis['__ENV__']?.['RELEASE'];
const ENVIRONMENT = globalThis.process?.env['ENVIRONMENT'] ?? globalThis['__ENV__']?.['ENVIRONMENT'];
const SENTRY_ENABLED = globalThis.process?.env['SENTRY_ENABLED'] ?? globalThis['__ENV__']?.['SENTRY_ENABLED'];
export const config: Parameters<typeof init>[0] = {
serverName: 'app',
enabled: String(SENTRY_ENABLED) === '1',
environment: ENVIRONMENT || 'local',
release: RELEASE || 'local',
enabled: SENTRY_ENABLED === '1',
environment: ENVIRONMENT ?? 'local',
release: RELEASE ?? 'local',
dsn: SENTRY_DSN,
};

View file

@ -21,6 +21,7 @@ import { Logo } from './Logo';
import { Feedback } from './Feedback';
import { UserSettings } from './UserSettings';
import ThemeButton from './ThemeButton';
import { getDocsUrl } from '@/lib/docs-url';
export interface NavigationItem {
label: string;
@ -121,6 +122,8 @@ export function Navigation() {
const me = meQuery.data?.me;
const docsUrl = getDocsUrl();
return (
<nav tw="bg-white shadow-md dark:bg-gray-900 z-10">
<div tw="mx-auto px-2 sm:px-6 lg:px-8">
@ -149,11 +152,13 @@ export function Navigation() {
</div>
</div>
</div>
<div tw="flex flex-row items-center space-x-4">
<ChakraLink tw="text-sm dark:text-gray-200" href={process.env.NEXT_PUBLIC_DOCS_LINK}>
Documentation
</ChakraLink>
</div>
{docsUrl ? (
<div tw="flex flex-row items-center space-x-4">
<ChakraLink tw="text-sm dark:text-gray-200" href={docsUrl}>
Documentation
</ChakraLink>
</div>
) : null}
<Divider orientation="vertical" tw="height[20px] ml-8 mr-3" />
<div tw="inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:pr-0">
<div tw="ml-3 relative">

View file

@ -12,6 +12,7 @@ import {
import clsx from 'clsx';
import { OrganizationType } from '@/graphql';
import { gql, DocumentType } from 'urql';
import { getDocsUrl } from '@/lib/docs-url';
const GetStartedWizard_GetStartedProgress = gql(/* GraphQL */ `
fragment GetStartedWizard_GetStartedProgress on OrganizationGetStarted {
@ -104,37 +105,25 @@ function GetStartedWizard({
<DrawerBody>
<p>Complete these steps to experience the full power of GraphQL Hive</p>
<div className="mt-4 flex flex-col divide-y-2 divide-gray-900">
<Task link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/projects`} completed={tasks.creatingProject}>
<Task link={getDocsUrl(`/get-started/projects`)} completed={tasks.creatingProject}>
Create a project
</Task>
<Task
link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/publish-schema`}
completed={tasks.publishingSchema}
>
<Task link={getDocsUrl(`/features/publish-schema`)} completed={tasks.publishingSchema}>
Publish a schema
</Task>
<Task
link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/checking-schema`}
completed={tasks.checkingSchema}
>
<Task link={getDocsUrl(`/features/checking-schema`)} completed={tasks.checkingSchema}>
Check a schema
</Task>
{'invitingMembers' in tasks && typeof tasks.invitingMembers === 'boolean' ? (
<Task
link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/get-started/organizations#members`}
completed={tasks.invitingMembers}
>
<Task link={getDocsUrl(`/get-started/organizations#members`)} completed={tasks.invitingMembers}>
Invite members
</Task>
) : null}
<Task
link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/monitoring`}
completed={tasks.reportingOperations}
>
<Task link={getDocsUrl(`/features/monitoring`)} completed={tasks.reportingOperations}>
Report operations
</Task>
<Task
link={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/checking-schema#with-usage-enabled`}
link={getDocsUrl(`/features/checking-schema#with-usage-enabled`)}
completed={tasks.enablingUsageBasedBreakingChanges}
>
Enable usage-based breaking changes
@ -152,11 +141,11 @@ function Task({
link,
}: React.PropsWithChildren<{
completed: boolean;
link: string;
link: string | null;
}>) {
return (
<a
href={link}
href={link ?? undefined}
target="_blank"
rel="noreferrer"
className={clsx('flex flex-row items-center gap-4 p-3 text-left', completed ? 'opacity-50' : 'hover:opacity-80')}

View file

@ -17,6 +17,7 @@ import { useRouteSelector } from '@/lib/hooks/use-route-selector';
import { canAccessOrganization, OrganizationAccessScope, useOrganizationAccess } from '@/lib/access/organization';
import cookies from 'js-cookie';
import { LAST_VISITED_ORG_KEY } from '@/constants';
import { getIsStripeEnabled } from '@/lib/billing/stripe-public-key';
enum TabValue {
Overview = 'overview',
@ -146,7 +147,7 @@ export function OrganizationLayout({
</Tabs.Trigger>
</NextLink>
)}
{canAccessOrganization(OrganizationAccessScope.Settings, me) && (
{getIsStripeEnabled() && canAccessOrganization(OrganizationAccessScope.Settings, me) && (
<NextLink passHref href={`/${orgId}/${TabValue.Subscription}`}>
<Tabs.Trigger value={TabValue.Subscription} asChild>
<a>Subscription</a>

View file

@ -2,6 +2,7 @@ import { ReactElement } from 'react';
import Image from 'next/image';
import { Card, Heading, Link } from '@/components/v2/index';
import { getDocsUrl } from '@/lib/docs-url';
import magnifier from '../../../public/images/figures/magnifier.svg';
export const EmptyList = ({
@ -11,16 +12,18 @@ export const EmptyList = ({
}: {
title: string;
description: string;
docsUrl: string;
docsUrl: string | null;
}): ReactElement => {
return (
<Card className="flex grow flex-col items-center gap-y-2">
<Image src={magnifier} alt="Magnifier illustration" width="200" height="200" className="drag-none" />
<Heading>{title}</Heading>
<span className="text-center text-sm font-medium text-gray-500">{description}</span>
<Link variant="primary" href={docsUrl} target="_blank" rel="noreferrer" className="my-5">
Read about it in the documentation
</Link>
{docsUrl === null ? null : (
<Link variant="primary" href={docsUrl} target="_blank" rel="noreferrer" className="my-5">
Read about it in the documentation
</Link>
)}
</Card>
);
};
@ -29,6 +32,6 @@ export const noSchema = (
<EmptyList
title="Hive is waiting for your first schema"
description="You can publish a schema with Hive CLI and Hive Client"
docsUrl={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/publish-schema`}
docsUrl={getDocsUrl(`/features/publish-schema`)}
/>
);

View file

@ -19,6 +19,7 @@ import {
} from '@/components/v2/icon';
import { CreateOrganizationModal } from '@/components/v2/modals';
import { MeDocument, OrganizationsDocument, OrganizationsQuery, OrganizationType } from '@/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
type DropdownOrganization = OrganizationsQuery['organizations']['nodes'];
@ -72,6 +73,7 @@ export const Header = (): ReactElement => {
};
}, [isOpaque]);
const docsUrl = getDocsUrl();
return (
<header
className={clsx(
@ -142,12 +144,14 @@ export const Header = (): ReactElement => {
</DropdownMenu.Item>
</a>
</NextLink>
<DropdownMenu.Item asChild>
<a href={process.env.NEXT_PUBLIC_DOCS_LINK} target="_blank" rel="noreferrer">
<FileTextIcon className="h-5 w-5" />
Documentation
</a>
</DropdownMenu.Item>
{docsUrl ? (
<DropdownMenu.Item asChild>
<a href={docsUrl} target="_blank" rel="noreferrer">
<FileTextIcon className="h-5 w-5" />
Documentation
</a>
</DropdownMenu.Item>
) : null}
<DropdownMenu.Item asChild>
<a href="https://status.graphql-hive.com" target="_blank" rel="noreferrer">
<AlertTriangleIcon className="h-5 w-5" />

View file

@ -1,6 +1,7 @@
import { ReactElement } from 'react';
import { Button, CopyValue, Heading, Link, Modal, Tag } from '@/components/v2';
import { getDocsUrl } from '@/lib/docs-url';
export const ConnectLabModal = ({
isOpen,
@ -11,6 +12,8 @@ export const ConnectLabModal = ({
toggleModalOpen: () => void;
endpoint: string;
}): ReactElement => {
const docsUrl = getDocsUrl('/features/tokens');
return (
<Modal open={isOpen} onOpenChange={toggleModalOpen} className="flex w-[650px] flex-col gap-5">
<Heading className="text-center">Connect to Lab</Heading>
@ -22,23 +25,13 @@ export const ConnectLabModal = ({
<span className="text-sm text-gray-500">To authenticate, use the following HTTP headers:</span>
<Tag>
X-Hive-Key:{' '}
<Link
variant="secondary"
target="_blank"
rel="noreferrer"
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/tokens`}
>
<Link variant="secondary" target="_blank" rel="noreferrer" href={docsUrl ?? undefined}>
YOUR_TOKEN_HERE
</Link>
</Tag>
<p className="text-sm text-gray-500">
Read the{' '}
<Link
variant="primary"
target="_blank"
rel="noreferrer"
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/tokens`}
>
<Link variant="primary" target="_blank" rel="noreferrer" href={docsUrl ?? undefined}>
Managing Tokens
</Link>{' '}
chapter in our documentation.

View file

@ -4,6 +4,7 @@ import { useMutation, useQuery } from 'urql';
import { Button, CopyValue, Heading, Link, Modal, Tag } from '@/components/v2';
import { CreateCdnTokenDocument, ProjectDocument, ProjectType } from '@/graphql';
import { getDocsUrl } from '@/lib/docs-url';
import { useRouteSelector } from '@/lib/hooks/use-route-selector';
const taxonomy = {
@ -83,7 +84,7 @@ export const ConnectSchemaModal = ({
variant="primary"
target="_blank"
rel="noreferrer"
href={`${process.env.NEXT_PUBLIC_DOCS_LINK}/features/registry-usage#apollo-federation`}
href={getDocsUrl(`/features/registry-usage#apollo-federation`) ?? undefined}
>
Using the Registry with a Apollo Gateway
</Link>{' '}

View file

@ -1,8 +1,19 @@
export const appInfo = {
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
appName: 'GraphQL Hive',
apiDomain: process.env['NEXT_PUBLIC_APP_BASE_URL'],
websiteDomain: process.env['NEXT_PUBLIC_APP_BASE_URL'],
apiBasePath: '/api/auth',
websiteBasePath: '/auth',
function throwException(msg: string) {
throw new Error(msg);
}
export const appInfo = () => {
const appBaseUrl =
globalThis.process?.env?.['APP_BASE_URL'] ??
globalThis?.['__ENV__']?.['APP_BASE_URL'] ??
throwException('APP_BASE_URL is not defined');
return {
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
appName: 'GraphQL Hive',
apiDomain: appBaseUrl,
websiteDomain: appBaseUrl,
apiBasePath: '/api/auth',
websiteBasePath: '/auth',
};
};

View file

@ -35,10 +35,10 @@ type LegacyAuth0ConfigEnabled = zod.TypeOf<typeof LegacyAuth0ConfigEnabledModel>
const GitHubConfigModel = zod.union([
zod.object({
NEXT_PUBLIC_AUTH_GITHUB: zod.union([zod.void(), zod.literal('0')]),
AUTH_GITHUB: zod.union([zod.void(), zod.literal('0')]),
}),
zod.object({
NEXT_PUBLIC_AUTH_GITHUB: zod.literal('1'),
AUTH_GITHUB: zod.literal('1'),
AUTH_GITHUB_CLIENT_ID: zod.string(),
AUTH_GITHUB_CLIENT_SECRET: zod.string(),
}),
@ -46,10 +46,10 @@ const GitHubConfigModel = zod.union([
const GoogleConfigModel = zod.union([
zod.object({
NEXT_PUBLIC_AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0')]),
AUTH_GOOGLE: zod.union([zod.void(), zod.literal('0')]),
}),
zod.object({
NEXT_PUBLIC_AUTH_GOOGLE: zod.literal('1'),
AUTH_GOOGLE: zod.literal('1'),
AUTH_GOOGLE_CLIENT_ID: zod.string(),
AUTH_GOOGLE_CLIENT_SECRET: zod.string(),
}),
@ -72,7 +72,7 @@ export const backendConfig = (): TypeInput => {
});
const providers: Array<TypeProvider> = [];
if (githubConfig['NEXT_PUBLIC_AUTH_GITHUB'] === '1') {
if (githubConfig['AUTH_GITHUB'] === '1') {
providers.push(
ThirdPartyEmailPasswordNode.Github({
clientId: githubConfig['AUTH_GITHUB_CLIENT_ID'],
@ -80,7 +80,7 @@ export const backendConfig = (): TypeInput => {
})
);
}
if (googleConfig['NEXT_PUBLIC_AUTH_GOOGLE'] === '1') {
if (googleConfig['AUTH_GOOGLE'] === '1') {
providers.push(
ThirdPartyEmailPasswordNode.Google({
clientId: googleConfig['AUTH_GOOGLE_CLIENT_ID'],
@ -94,7 +94,7 @@ export const backendConfig = (): TypeInput => {
connectionURI: superTokensConfig['SUPERTOKENS_CONNECTION_URI'],
apiKey: superTokensConfig['SUPERTOKENS_API_KEY'],
},
appInfo,
appInfo: appInfo(),
recipeList: [
ThirdPartyEmailPasswordNode.init({
providers,

View file

@ -7,15 +7,15 @@ import { appInfo } from './app-info';
export const frontendConfig = () => {
const providers: Array<Provider> = [];
if (process.env['NEXT_PUBLIC_AUTH_GITHUB'] === '1') {
if (globalThis['__ENV__']?.['AUTH_GITHUB'] === '1') {
providers.push(ThirdPartyEmailPasswordReact.Github.init());
}
if (process.env['NEXT_PUBLIC_AUTH_GOOGLE'] === '1') {
if (globalThis['__ENV__']?.['AUTH_GOOGLE'] === '1') {
providers.push(ThirdPartyEmailPasswordReact.Google.init());
}
return {
appInfo,
appInfo: appInfo(),
recipeList: [
ThirdPartyEmailPasswordReact.init({
signInAndUpFeature: {

View file

@ -1,5 +1,6 @@
export const LAST_VISITED_ORG_KEY = 'lastVisitedOrganization';
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_TRACKING_ID;
export const GA_TRACKING_ID = globalThis.process?.env['GA_TRACKING_ID'] ?? globalThis['__ENV__']?.['GA_TRACKING_ID'];
export const CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID;
export const CRISP_WEBSITE_ID =
globalThis.process?.env['CRISP_WEBSITE_ID'] ?? globalThis['__ENV__']?.['CRISP_WEBSITE_ID'];

View file

@ -0,0 +1,9 @@
export const getStripePublicKey = () => {
const stripePublicUrl = globalThis.process?.env['STRIPE_PUBLIC_KEY'] ?? globalThis['__ENV__']?.['STRIPE_PUBLIC_KEY'];
if (!stripePublicUrl) {
return null;
}
return stripePublicUrl;
};
export const getIsStripeEnabled = () => !!getStripePublicKey();

View file

@ -1,15 +1,20 @@
import { Elements as ElementsProvider } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import React from 'react';
const STRIPE_PUBLIC_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || null;
const stripePromise$ = !STRIPE_PUBLIC_KEY ? null : loadStripe(STRIPE_PUBLIC_KEY);
import { getStripePublicKey } from './stripe-public-key';
import { loadStripe } from '@stripe/stripe-js';
export const HiveStripeWrapper: React.FC<{}> = ({ children }) => {
if (STRIPE_PUBLIC_KEY === null || stripePromise$ === null) {
const [stripe] = React.useState(() => {
const stripePublicKey = getStripePublicKey();
return stripePublicKey ? loadStripe(stripePublicKey) : null;
});
if (stripe === null) {
return children as any;
}
return <ElementsProvider stripe={stripePromise$}>{children}</ElementsProvider>;
return (
<React.Suspense fallback={() => <>{children}</>}>
<ElementsProvider stripe={stripe}>{children}</ElementsProvider>
</React.Suspense>
);
};

View file

@ -0,0 +1,7 @@
export const getDocsUrl = (path = '') => {
const docsUrl = globalThis.process?.env['DOCS_URL'] ?? globalThis['__ENV__']?.['DOCS_URL'];
if (!docsUrl) {
return null;
}
return `${docsUrl}${path}`;
};

View file

@ -18,13 +18,7 @@
"outputs": ["dist/**"]
},
"@hive/app#build": {
"dependsOn": [
"^build",
"$NEXT_PUBLIC_DOCS_LINK",
"$NEXT_PUBLIC_CRISP_WEBSITE_ID",
"$NEXT_PUBLIC_GA_TRACKING_ID",
"$NEXT_PUBLIC_STRIPE_PUBLIC_KEY"
],
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"@hive/docs#build": {