Compare commits

..

38 Commits

Author SHA1 Message Date
Szilárd Dóró
8ca1f92491 Merge pull request #1525 from nhost/changeset-release/main
chore: update versions
2023-01-17 14:20:25 +01:00
github-actions[bot]
5535b9085b chore: update versions 2023-01-17 10:20:52 +00:00
Szilárd Dóró
bc51122b25 Merge pull request #1522 from nhost/fix/retrigger-deployment-status
fix(dashboard): correct redeployment button
2023-01-17 11:19:38 +01:00
Szilárd Dóró
b060e5e550 fix(dashboard): restore deployment timer 2023-01-17 09:27:28 +01:00
Szilárd Dóró
6a906b22e2 fix(dashboard): show deployment duration 2023-01-17 09:04:43 +01:00
Pilou
860c9d1be4 Merge pull request #1523 from akd-io/patch-2
Docs: Fix npm install command
2023-01-16 18:33:44 +01:00
Anders Kjær Damgaard
9eec3e58f5 Fix npm install command
Was missing a space
2023-01-16 15:42:56 +01:00
Johan Eliasson
4e01a43e94 Merge pull request #1431 from nhost/example-updates
codegen example updates
2023-01-16 14:31:27 +01:00
Szilárd Dóró
c126b20dcf chore(dashboard): add changeset 2023-01-16 13:38:00 +01:00
Szilárd Dóró
b727a24a5f fix(dashboard): restore "Redeploy" button behavior 2023-01-16 12:37:32 +01:00
Szilárd Dóró
ecadd7e1b9 fix(dashboard): don't show redeploy button when deployment is in progress 2023-01-16 11:52:58 +01:00
Johan Eliasson
2d661174a8 update 2023-01-16 10:01:02 +01:00
Johan Eliasson
fcb3e5192f Merge branch 'main' into example-updates 2023-01-16 10:00:28 +01:00
Szilárd Dóró
66fdc63f38 Merge pull request #1516 from nhost/renovate/rimraf-4.x
chore(deps): update dependency rimraf to v4
2023-01-13 10:25:34 +01:00
renovate[bot]
fa37cb6171 chore(deps): update dependency rimraf to v4 2023-01-13 02:39:43 +00:00
Szilárd Dóró
c1bea1294d Merge pull request #1512 from nhost/changeset-release/main
chore: update versions
2023-01-12 15:46:08 +01:00
github-actions[bot]
8af2f6e9dd chore: update versions 2023-01-12 11:43:39 +00:00
Szilárd Dóró
e3d0b96917 Merge pull request #1503 from nhost/feat/retrigger-deployments
feat(dashboard): Retrigger Deployments
2023-01-12 12:41:55 +01:00
Szilárd Dóró
36c3519cf8 chore(dashboard): retrigger deployments 2023-01-12 10:18:28 +01:00
Szilárd Dóró
9b52e9bf13 Merge branch 'main' into feat/retrigger-deployments 2023-01-12 09:49:46 +01:00
Szilárd Dóró
07c8d90053 fix(dashboard): lint errors 2023-01-11 11:13:37 +01:00
Szilárd Dóró
a2621e40a4 feat(dashboard): unified list items for deployments
fixed the way the latest scheduled or pending deployment is tracked
2023-01-11 10:37:10 +01:00
Szilárd Dóró
61120a137a feat(dashboard): add redeployment support to overview 2023-01-11 09:43:06 +01:00
Szilárd Dóró
faea8feb2e Merge branch 'main' into feat/retrigger-deployments 2023-01-11 08:56:05 +01:00
Szilárd Dóró
552e31a4f0 feat(dashboard): initial redeploy button code 2023-01-10 17:12:14 +01:00
Johan Eliasson
c4561cae38 redirect 2023-01-07 10:20:29 +01:00
Johan Eliasson
599387934c update 2022-12-31 09:37:45 +01:00
Johan Eliasson
04cea41111 removed package 2022-12-31 08:20:02 +01:00
Johan Eliasson
dc3723306d updated lock file 2022-12-30 11:38:48 +01:00
Johan Eliasson
d7fa572ab6 Merge branch 'main' into example-updates 2022-12-30 11:38:08 +01:00
Johan Eliasson
c21118257f updated lock file 2022-12-25 21:43:27 +01:00
Johan Eliasson
4712b7ff68 updated readme files 2022-12-25 21:40:13 +01:00
Johan Eliasson
4f305a8985 update 2022-12-25 21:33:12 +01:00
Johan Eliasson
cd7d133ba3 updated README 2022-12-25 21:32:30 +01:00
Johan Eliasson
2927a9ac31 move 2022-12-25 21:31:00 +01:00
Johan Eliasson
695eaa77ca update 2022-12-25 21:29:18 +01:00
Johan Eliasson
a29d21e194 react apollo updated 2022-12-25 15:21:52 +01:00
Johan Eliasson
cd20bd4ef2 urql fixes + apollo metadata updates 2022-12-25 14:57:11 +01:00
191 changed files with 11234 additions and 3136 deletions

View File

@@ -1,5 +1,17 @@
# @nhost/dashboard
## 0.9.7
### Patch Changes
- c126b20d: fix(dashboard): correct redeployment button
## 0.9.6
### Patch Changes
- 36c3519c: feat(dashboard): retrigger deployments
## 0.9.5
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.9.5",
"version": "0.9.7",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -1,21 +1,14 @@
import type { DeploymentRowFragment } from '@/generated/graphql';
import { useGetDeploymentsSubSubscription } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import DelayedLoading from '@/ui/DelayedLoading';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import { getLastLiveDeployment } from '@/utils/helpers';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import {
differenceInSeconds,
formatDistanceToNowStrict,
parseISO,
} from 'date-fns';
useGetDeploymentsSubSubscription,
useLatestLiveDeploymentSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/generated/graphql';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import List from '@/ui/v2/List';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
type AppDeploymentsProps = {
appId: string;
@@ -66,146 +59,8 @@ function NextPrevPageLink(props: NextPrevPageLinkProps) {
);
}
type AppDeploymentDurationProps = {
startedAt: string;
endedAt: string;
};
export function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{
fontVariantNumeric: 'tabular-nums',
}}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{durationMins}m {durationSecs}s
</div>
);
}
type AppDeploymentRowProps = {
deployment: DeploymentRowFragment;
isDeploymentLive: boolean;
};
export function AppDeploymentRow({
deployment,
isDeploymentLive,
}: AppDeploymentRowProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { commitMessage } = deployment;
return (
<div className="flex flex-row items-center px-2 py-4">
<div className="mr-2 flex items-center justify-center">
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="mx-4 w-full">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<a
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="max-w-md truncate text-sm+ font-normal text-greyscaleDark">
{commitMessage?.trim() || (
<span className="pr-1 font-normal italic">
No commit message
</span>
)}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
</div>
</a>
</Link>
</div>
<div className="flex flex-row">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-28 self-center text-right font-mono text-sm- font-medium">
<a
className="font-mono font-medium text-greyscaleDark"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
>
{deployment.commitSHA.substring(0, 7)}
</a>
</div>
<div className="mx-4 w-28 text-right">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</Link>
</div>
</div>
</div>
);
}
export default function AppDeployments(props: AppDeploymentsProps) {
const { appId } = props;
const [idOfLiveDeployment, setIdOfLiveDeployment] = useState('');
const router = useRouter();
@@ -216,36 +71,59 @@ export default function AppDeployments(props: AppDeploymentsProps) {
const limit = 10;
const offset = (page - 1) * limit;
// @TODO: Should query for all deployments, then subscribe to new ones.
const { data, loading, error } = useGetDeploymentsSubSubscription({
variables: {
id: appId,
limit,
offset,
},
const {
data: deploymentPageData,
loading: deploymentPageLoading,
error,
} = useGetDeploymentsSubSubscription({
variables: { id: appId, limit, offset },
});
useEffect(() => {
if (!data) {
return;
}
const { data: latestDeploymentData, loading: latestDeploymentLoading } =
useGetDeploymentsSubSubscription({
variables: { id: appId, limit: 1, offset: 0 },
});
if (page === 1) {
setIdOfLiveDeployment(getLastLiveDeployment(data.deployments));
}
}, [data, idOfLiveDeployment, loading, page]);
const {
data: latestLiveDeploymentData,
loading: latestLiveDeploymentLoading,
} = useLatestLiveDeploymentSubSubscription({ variables: { appId } });
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({ variables: { appId } });
const loading =
deploymentPageLoading ||
scheduledOrPendingDeploymentsLoading ||
latestDeploymentLoading ||
latestLiveDeploymentLoading;
if (loading) {
return <DelayedLoading delay={500} className="mt-12" />;
return (
<ActivityIndicator
delay={500}
className="mt-12"
label="Loading deployments..."
/>
);
}
if (error) {
throw error;
}
const nrOfDeployments = data.deployments.length;
const { deployments } = deploymentPageData || {};
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData || {};
const latestDeployment = latestDeploymentData?.deployments[0];
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
const nrOfDeployments = deployments?.length || 0;
const nextAllowed = !(nrOfDeployments < limit);
const liveDeploymentId = latestLiveDeployment?.id || '';
return (
<div className="mt-6">
@@ -253,15 +131,17 @@ export default function AppDeployments(props: AppDeploymentsProps) {
<p className="text-sm text-greyscaleGrey">No deployments yet.</p>
) : (
<div>
<div className="mt-3 divide-y-1 border-t border-b">
{data.deployments.map((deployment) => (
<AppDeploymentRow
deployment={deployment}
<List className="mt-3 divide-y-1 border-t border-b">
{deployments.map((deployment) => (
<DeploymentListItem
key={deployment.id}
isDeploymentLive={idOfLiveDeployment === deployment.id}
deployment={deployment}
isLive={liveDeploymentId === deployment.id}
showRedeploy={latestDeployment.id === deployment.id}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</div>
</List>
<div className="mt-8 flex w-full justify-center">
<div className="flex items-center">
<NextPrevPageLink

View File

@@ -34,13 +34,13 @@ export function Repo({ repo, setSelectedRepoId }: RepoProps) {
const [updateApp, { loading, error }] = useUpdateAppMutation({
refetchQueries: [
refetchGetAppByWorkspaceAndNameQuery({
workspace: currentWorkspace.slug,
slug: currentApplication.slug,
workspace: currentWorkspace?.slug,
slug: currentApplication?.slug,
}),
],
});
const { githubRepository } = currentApplication;
const { githubRepository } = currentApplication || {};
const isThisRepositoryAlreadyConnected =
githubRepository?.fullName && githubRepository.fullName === repo.fullName;

View File

@@ -0,0 +1,60 @@
import { differenceInSeconds, parseISO } from 'date-fns';
import { useEffect, useState } from 'react';
export interface AppDeploymentDurationProps {
/**
* Start date of the deployment.
*/
startedAt: string;
/**
* End date of the deployment.
*/
endedAt?: string;
}
export default function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{ fontVariantNumeric: 'tabular-nums' }}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{Number.isNaN(durationMins) || Number.isNaN(durationSecs) ? (
<span>0m 0s</span>
) : (
<span>
{durationMins}m {durationSecs}s
</span>
)}
</div>
);
}

View File

@@ -0,0 +1,2 @@
export * from './AppDeploymentDuration';
export { default } from './AppDeploymentDuration';

View File

@@ -0,0 +1,161 @@
import NavLink from '@/components/common/NavLink';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import Button from '@/ui/v2/Button';
import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import { ListItem } from '@/ui/v2/ListItem';
import Tooltip from '@/ui/v2/Tooltip';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeploymentListItemProps {
/**
* Deployment data.
*/
deployment: DeploymentRowFragment;
/**
* Determines whether or not the deployment is live.
*/
isLive?: boolean;
/**
* Determines whether or not the redeploy button should be shown for the
* deployment.
*/
showRedeploy?: boolean;
/**
* Determines whether or not the redeploy button is disabled.
*/
disableRedeploy?: boolean;
}
export default function DeploymentListItem({
deployment,
isLive,
showRedeploy,
disableRedeploy,
}: DeploymentListItemProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const relativeDateOfDeployment = deployment.deploymentStartedAt
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
const [insertDeployment, { loading }] = useInsertDeploymentMutation();
const { commitMessage } = deployment;
return (
<ListItem.Root>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<ListItem.Avatar>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8 shrink-0"
/>
</ListItem.Avatar>
<ListItem.Text
primary={
commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)
}
secondary={relativeDateOfDeployment}
/>
</div>
<div className="grid grid-flow-col gap-2 items-center">
{showRedeploy && (
<Tooltip
title="Deployments cannot be re-triggered when a deployment is in progress."
hasDisabledChildren={disableRedeploy || loading}
disableHoverListener={!disableRedeploy}
>
<Button
disabled={disableRedeploy || loading}
size="small"
color="secondary"
variant="outlined"
onClick={async (event) => {
event.stopPropagation();
event.preventDefault();
const insertDeploymentPromise = insertDeployment({
variables: {
object: {
appId: currentApplication?.id,
commitMessage: deployment.commitMessage,
commitSHA: deployment.commitSHA,
commitUserAvatarUrl: deployment.commitUserAvatarUrl,
commitUserName: deployment.commitUserName,
deploymentStatus: 'SCHEDULED',
},
},
});
await toast.promise(
insertDeploymentPromise,
{
loading: 'Scheduling deployment...',
success: 'Deployment has been scheduled successfully.',
error: 'An error occurred when scheduling deployment.',
},
toastStyleProps,
);
}}
startIcon={
<ArrowCounterclockwiseIcon className={twMerge('w-4 h-4')} />
}
className="rounded-full py-1 px-2 text-xs"
>
Redeploy
</Button>
</Tooltip>
)}
{isLive && (
<div className="w-12 flex justify-end">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-16 text-right font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
<div className="w-[80px] text-right font-mono text-sm- font-medium">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
<ChevronRightIcon className="h-4 w-4" />
</div>
</ListItem.Button>
</ListItem.Root>
);
}

View File

@@ -0,0 +1,2 @@
export * from './DeploymentListItem';
export { default } from './DeploymentListItem';

View File

@@ -1,25 +1,21 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import { useDialog } from '@/components/common/DialogProvider';
import NavLink from '@/components/common/NavLink';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import GithubIcon from '@/components/icons/GithubIcon';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import type { ListItemRootProps } from '@/ui/v2/ListItem';
import { ListItem } from '@/ui/v2/ListItem';
import List from '@/ui/v2/List';
import Text from '@/ui/v2/Text';
import { getLastLiveDeployment } from '@/utils/helpers';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useGetDeploymentsSubSubscription } from '@/utils/__generated__/graphql';
import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
function OverviewDeploymentsTopBar() {
@@ -54,92 +50,6 @@ function OverviewDeploymentsTopBar() {
);
}
interface OverviewDeployProps extends ListItemRootProps {
/**
* Deployment metadata to display.
*/
deployment: DeploymentRowFragment;
/**
* Determines to show a status badge showing the live status of a deployment reflecting the latest state of the application.
*/
isDeploymentLive: boolean;
}
function OverviewDeployment({
deployment,
isDeploymentLive,
className,
}: OverviewDeployProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const relativeDateOfDeployment = formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
);
const { commitMessage } = deployment;
return (
<ListItem.Root className={twMerge('grid grid-flow-row', className)}>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<div>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="grid grid-flow-row truncate text-sm+ font-medium">
<Text className="inline cursor-pointer truncate font-medium leading-snug text-greyscaleDark">
{commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)}
</Text>
<Text className="text-sm font-normal leading-[1.375rem] text-greyscaleGrey">
{relativeDateOfDeployment}
</Text>
</div>
</div>
<div className="grid grid-flow-col items-center self-center">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-20 self-center text-right align-middle font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
<div className="w-20 self-center text-right align-middle font-mono text-sm-">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</ListItem.Button>
</ListItem.Root>
);
}
interface OverviewDeploymentsProps {
projectId: string;
githubRepository: { fullName: string };
@@ -159,9 +69,18 @@ function OverviewDeployments({
},
});
if (loading) {
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId: projectId,
},
});
if (loading || scheduledOrPendingDeploymentsLoading) {
return (
<div style={{ height: '240px' }}>
<div className="h-60">
<ActivityIndicator label="Loading deployments..." />
</div>
);
@@ -218,22 +137,22 @@ function OverviewDeployments({
);
}
const getLastLiveDeploymentId = getLastLiveDeployment(deployments);
const liveDeploymentId = getLastLiveDeployment(deployments);
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData;
return (
<div className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment) => {
const isDeploymentLive = deployment.id === getLastLiveDeploymentId;
return (
<OverviewDeployment
key={deployment.id}
deployment={deployment}
isDeploymentLive={isDeploymentLive}
/>
);
})}
</div>
<List className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment, index) => (
<DeploymentListItem
key={deployment.id}
deployment={deployment}
isLive={deployment.id === liveDeploymentId}
showRedeploy={index === 0}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</List>
);
}

View File

@@ -1,9 +1,11 @@
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
export type DeploymentStatus =
| 'DEPLOYING'
| 'DEPLOYED'
| 'FAILED'
| 'PENDING'
| 'SCHEDULED'
| undefined
| null;
@@ -12,32 +14,30 @@ type StatusCircleProps = {
className?: string;
};
export function StatusCircle(props: StatusCircleProps) {
const { status, className } = props;
export function StatusCircle({ status, className }: StatusCircleProps) {
const baseClasses = 'w-1.5 h-1.5 rounded-full';
if (!status) {
const classes = clsx(baseClasses, 'bg-gray-300', className);
return <div className={classes} />;
}
if (status === 'DEPLOYING') {
const classes = clsx(baseClasses, 'bg-yellow-300', className);
return <div className={classes} />;
if (status === 'DEPLOYING' || status === 'PENDING') {
return (
<div
className={twMerge(
baseClasses,
'bg-yellow-300 animate-pulse',
className,
)}
/>
);
}
if (status === 'DEPLOYED') {
const classes = clsx(baseClasses, 'bg-green-300', className);
return <div className={classes} />;
return <div className={twMerge(baseClasses, 'bg-green-300', className)} />;
}
if (status === 'FAILED') {
const classes = clsx(baseClasses, 'bg-red', className);
return <div className={classes} />;
return <div className={twMerge(baseClasses, 'bg-red', className)} />;
}
return null;
return <div className={twMerge(baseClasses, 'bg-gray-300', className)} />;
}
export default StatusCircle;

View File

@@ -0,0 +1,17 @@
import { styled } from '@mui/material';
import type { ListItemAvatarProps as MaterialListItemAvatarProps } from '@mui/material/ListItemAvatar';
import MaterialListItemAvatar from '@mui/material/ListItemAvatar';
export interface ListItemAvatarProps extends MaterialListItemAvatarProps {}
const StyledListItemAvatar = styled(MaterialListItemAvatar)({
minWidth: 0,
});
function ListItemAvatar({ children, ...props }: ListItemAvatarProps) {
return <StyledListItemAvatar {...props}>{children}</StyledListItemAvatar>;
}
ListItemAvatar.displayName = 'NhostListItemAvatar';
export default ListItemAvatar;

View File

@@ -1,8 +1,11 @@
import ListItemAvatar from './ListItemAvatar';
import ListItemButton from './ListItemButton';
import ListItemIcon from './ListItemIcon';
import ListItemRoot from './ListItemRoot';
import ListItemText from './ListItemText';
export * from './ListItemAvatar';
export { default as ListItemAvatar } from './ListItemAvatar';
export * from './ListItemButton';
export { default as ListItemButton } from './ListItemButton';
export * from './ListItemIcon';
@@ -13,6 +16,7 @@ export * from './ListItemText';
export { default as ListItemText } from './ListItemText';
export const ListItem = {
Avatar: ListItemAvatar,
Root: ListItemRoot,
Button: ListItemButton,
Icon: ListItemIcon,

View File

@@ -0,0 +1,34 @@
import type { IconProps } from '@/ui/v2/icons';
import SvgIcon from '@mui/material/SvgIcon';
function ArrowCounterclockwiseIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="A counterclockwise arrow"
{...props}
>
<path
d="M4.99 6.232h-3v-3"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M4.11 11.89a5.5 5.5 0 1 0 0-7.78L1.99 6.233"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</SvgIcon>
);
}
ArrowCounterclockwiseIcon.displayName = 'NhostArrowCounterclockwiseIcon';
export default ArrowCounterclockwiseIcon;

View File

@@ -0,0 +1 @@
export { default } from './ArrowCounterclockwiseIcon';

View File

@@ -20,6 +20,34 @@ query getDeployments($id: uuid!, $limit: Int!, $offset: Int!) {
}
}
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {
deploymentStatus: { _in: ["PENDING", "SCHEDULED"] }
appId: { _eq: $appId }
}
) {
...DeploymentRow
}
}
subscription LatestLiveDeploymentSub($appId: uuid!) {
deployments(
where: { deploymentStatus: { _eq: "DEPLOYED" }, appId: { _eq: $appId } }
order_by: { deploymentEndedAt: desc }
limit: 1
offset: 0
) {
...DeploymentRow
}
}
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(
where: { appId: { _eq: $id } }

View File

@@ -1,4 +1,4 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useDeploymentSubSubscription } from '@/generated/graphql';
@@ -8,6 +8,7 @@ import DelayedLoading from '@/ui/DelayedLoading';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import { Text } from '@/ui/Text';
import Link from '@/ui/v2/Link';
import { format, formatDistanceToNowStrict, parseISO } from 'date-fns';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
@@ -50,6 +51,12 @@ export default function DeploymentDetailsPage() {
);
}
const relativeDateOfDeployment = deployment.deploymentStartedAt
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
return (
<Container>
<div className="flex justify-between">
@@ -104,24 +111,20 @@ export default function DeploymentDetailsPage() {
{deployment.commitMessage}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
{relativeDateOfDeployment}
</div>
</div>
</div>
<div className=" flex items-center">
<a
className="self-center font-mono text-sm- font-medium text-greyscaleDark"
<Link
className="self-center font-mono text-sm- font-medium"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
underline="hover"
>
{deployment.commitSHA.substring(0, 7)}
</a>
</Link>
<div className="w-20 text-right">
<AppDeploymentDuration
@@ -133,6 +136,10 @@ export default function DeploymentDetailsPage() {
</div>
<div>
<div className="rounded-lg bg-verydark p-4 text-sm- text-white">
{deployment.deploymentLogs.length === 0 && (
<span className="font-mono">No message.</span>
)}
{deployment.deploymentLogs.map((log) => (
<div key={log.id} className="flex font-mono">
<div className=" mr-2 flex-shrink-0">

View File

@@ -17215,6 +17215,27 @@ export type GetDeploymentsQueryVariables = Exact<{
export type GetDeploymentsQuery = { __typename?: 'query_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type ScheduledOrPendingDeploymentsSubSubscriptionVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type ScheduledOrPendingDeploymentsSubSubscription = { __typename?: 'subscription_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type LatestLiveDeploymentSubSubscriptionVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type LatestLiveDeploymentSubSubscription = { __typename?: 'subscription_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type InsertDeploymentMutationVariables = Exact<{
object: Deployments_Insert_Input;
}>;
export type InsertDeploymentMutation = { __typename?: 'mutation_root', insertDeployment?: { __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null } | null };
export type GetDeploymentsSubSubscriptionVariables = Exact<{
id: Scalars['uuid'];
limit: Scalars['Int'];
@@ -19147,6 +19168,106 @@ export type GetDeploymentsQueryResult = Apollo.QueryResult<GetDeploymentsQuery,
export function refetchGetDeploymentsQuery(variables: GetDeploymentsQueryVariables) {
return { query: GetDeploymentsDocument, variables: variables }
}
export const ScheduledOrPendingDeploymentsSubDocument = gql`
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {deploymentStatus: {_in: ["PENDING", "SCHEDULED"]}, appId: {_eq: $appId}}
) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
/**
* __useScheduledOrPendingDeploymentsSubSubscription__
*
* To run a query within a React component, call `useScheduledOrPendingDeploymentsSubSubscription` and pass it any options that fit your needs.
* When your component renders, `useScheduledOrPendingDeploymentsSubSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useScheduledOrPendingDeploymentsSubSubscription({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useScheduledOrPendingDeploymentsSubSubscription(baseOptions: Apollo.SubscriptionHookOptions<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>(ScheduledOrPendingDeploymentsSubDocument, options);
}
export type ScheduledOrPendingDeploymentsSubSubscriptionHookResult = ReturnType<typeof useScheduledOrPendingDeploymentsSubSubscription>;
export type ScheduledOrPendingDeploymentsSubSubscriptionResult = Apollo.SubscriptionResult<ScheduledOrPendingDeploymentsSubSubscription>;
export const LatestLiveDeploymentSubDocument = gql`
subscription LatestLiveDeploymentSub($appId: uuid!) {
deployments(
where: {deploymentStatus: {_eq: "DEPLOYED"}, appId: {_eq: $appId}}
order_by: {deploymentEndedAt: desc}
limit: 1
offset: 0
) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
/**
* __useLatestLiveDeploymentSubSubscription__
*
* To run a query within a React component, call `useLatestLiveDeploymentSubSubscription` and pass it any options that fit your needs.
* When your component renders, `useLatestLiveDeploymentSubSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLatestLiveDeploymentSubSubscription({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useLatestLiveDeploymentSubSubscription(baseOptions: Apollo.SubscriptionHookOptions<LatestLiveDeploymentSubSubscription, LatestLiveDeploymentSubSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LatestLiveDeploymentSubSubscription, LatestLiveDeploymentSubSubscriptionVariables>(LatestLiveDeploymentSubDocument, options);
}
export type LatestLiveDeploymentSubSubscriptionHookResult = ReturnType<typeof useLatestLiveDeploymentSubSubscription>;
export type LatestLiveDeploymentSubSubscriptionResult = Apollo.SubscriptionResult<LatestLiveDeploymentSubSubscription>;
export const InsertDeploymentDocument = gql`
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
export type InsertDeploymentMutationFn = Apollo.MutationFunction<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
/**
* __useInsertDeploymentMutation__
*
* To run a mutation, you first call `useInsertDeploymentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInsertDeploymentMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [insertDeploymentMutation, { data, loading, error }] = useInsertDeploymentMutation({
* variables: {
* object: // value for 'object'
* },
* });
*/
export function useInsertDeploymentMutation(baseOptions?: Apollo.MutationHookOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InsertDeploymentMutation, InsertDeploymentMutationVariables>(InsertDeploymentDocument, options);
}
export type InsertDeploymentMutationHookResult = ReturnType<typeof useInsertDeploymentMutation>;
export type InsertDeploymentMutationResult = Apollo.MutationResult<InsertDeploymentMutation>;
export type InsertDeploymentMutationOptions = Apollo.BaseMutationOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
export const GetDeploymentsSubDocument = gql`
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(

View File

@@ -13,6 +13,9 @@ module.exports = {
...defaultTheme.screens,
},
extend: {
animation: {
'spin-reverse': 'spin 1.5s linear infinite reverse',
},
colors: {
primary: '#0052cd',
'primary-light': '#ebf3ff',

View File

@@ -103,7 +103,7 @@ You can install the Nhost Next.js SDK with:
<TabItem value="npm" label="npm" default>
```bash
npm install@nhost/nextjs graphql
npm install @nhost/nextjs graphql
```
</TabItem>

View File

@@ -1,11 +1,12 @@
# GraphQL Code Generator Example with React and Apollo Client
This is an example repo for how to use GraphQL Code Generator together with:
Todo app to show how to use:
- [TypeScript](https://www.typescriptlang.org/)
- [Nhost](https://nhost.io/)
- [React](https://reactjs.org/)
- [TypeScript](https://www.typescriptlang.org/)
- [GraphQL Code Generator](https://the-guild.dev/graphql/codegen)
- [Apollo Client](https://www.apollographql.com/docs/react/)
- [Nhost](http://nhost.io/)
This repo is a reference repo for the blog post: [How to use GraphQL Code Generator with React and Apollo](https://nhost.io/blog/how-to-use-graphql-code-generator-with-react-and-apollo).
@@ -13,42 +14,42 @@ This repo is a reference repo for the blog post: [How to use GraphQL Code Genera
1. Clone the repository
```
```sh
git clone https://github.com/nhost/nhost
cd nhost
```
2. Install and build dependencies
```
```sh
pnpm install
pnpm build
pnpm run build
```
3. Go to the Codegen React Apollo example folder
3. Go to the example folder
```
cd examples/codegen-react-apollo
```sh
cd examples/codegen-react-urql
```
4. Terminal 1: Start Nhost
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
```sh
nhost up
nhost up -d
```
5. Terminal 2: Run GraphQL Codegen
5. Terminal 2: Start the React application
```sh
pnpm run dev
```
## GraphQL Code Generators
To re-run the GraphQL Code Generators, run the following:
```
pnpm codegen -w
```
> `-w` runs [codegen in watch mode](https://www.the-guild.dev/graphql/codegen/docs/getting-started/development-workflow#watch-mode).
6. Terminal 3: Start the React application
```sh
pnpm dev
```

View File

@@ -2,13 +2,3 @@ schema:
- http://localhost:1337/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
documents:
- 'src/**/*.graphql'
generates:
src/utils/__generated__/graphql.ts:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
config:
withRefetchFn: true

View File

@@ -9,6 +9,6 @@
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,14 +1,25 @@
metadata_directory: metadata
services:
hasura:
image: hasura/graphql-engine:v2.15.2
environment:
hasura_graphql_enable_remote_schema_permissions: false
image: hasura/graphql-engine:v2.15.2
minio:
environment:
minio_root_password: minioaccesskey123123
minio_root_user: minioaccesskey123123
postgres:
environment:
postgres_password: postgres
postgres_user: postgres
auth:
image: nhost/hasura-auth:0.16.2
image: nhost/hasura-auth:0.16.1
storage:
image: nhost/hasura-storage:0.3.0
auth:
webauthn:
enabled: true
rp_name: URQL
access_control:
email:
allowed_email_domains: ''
@@ -18,13 +29,13 @@ auth:
url:
allowed_redirect_urls: ''
anonymous_users_enabled: false
client_url: http://localhost:3000
client_url: http://localhost:5173
disable_new_users: false
email:
enabled: false
passwordless:
enabled: false
signin_email_verified_required: true
enabled: true
signin_email_verified_required: false
template_fetch_url: ''
gravatar:
default: ''
@@ -117,11 +128,7 @@ auth:
secure: false
sender: hasura-auth@example.com
user: user
token:
access:
expires_in: 900
refresh:
expires_in: 43200
access_token_expires_in: 315
user:
allowed_roles: user,me
default_allowed_roles: user,me

View File

@@ -9,6 +9,6 @@
connection_lifetime: 600
idle_timeout: 180
max_connections: 50
retries: 1
retries: 20
use_prepared_statements: true
tables: "!include default/tables/tables.yaml"

View File

@@ -2,8 +2,14 @@ table:
name: provider_requests
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
id:
custom_name: id
options:
custom_name: options
custom_column_names:
id: id
options: options
custom_name: authProviderRequests
custom_root_fields:
delete: deleteAuthProviderRequests

View File

@@ -2,8 +2,11 @@ table:
name: providers
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
id:
custom_name: id
custom_column_names:
id: id
custom_name: authProviders
custom_root_fields:
delete: deleteAuthProviders

View File

@@ -2,8 +2,11 @@ table:
name: roles
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
role:
custom_name: role
custom_column_names:
role: role
custom_name: authRoles
custom_root_fields:
delete: deleteAuthRoles

View File

@@ -1,30 +0,0 @@
table:
name: user_authenticators
schema: auth
configuration:
column_config:
credential_id:
custom_name: credentialId
credential_public_key:
custom_name: credentialPublicKey
user_id:
custom_name: userId
custom_column_names:
credential_id: credentialId
credential_public_key: credentialPublicKey
user_id: userId
custom_name: authUserAuthenticators
custom_root_fields:
delete: deleteAuthUserAuthenticators
delete_by_pk: deleteAuthUserAuthenticator
insert: insertAuthUserAuthenticators
insert_one: insertAuthUserAuthenticator
select: authUserAuthenticators
select_aggregate: authUserAuthenticatorsAggregate
select_by_pk: authUserAuthenticator
update: updateAuthUserAuthenticators
update_by_pk: updateAuthUserAuthenticator
object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id

View File

@@ -7,6 +7,8 @@ configuration:
custom_name: accessToken
created_at:
custom_name: createdAt
id:
custom_name: id
provider_id:
custom_name: providerId
provider_user_id:
@@ -20,6 +22,7 @@ configuration:
custom_column_names:
access_token: accessToken
created_at: createdAt
id: id
provider_id: providerId
provider_user_id: providerUserId
refresh_token: refreshToken

View File

@@ -5,10 +5,16 @@ configuration:
column_config:
created_at:
custom_name: createdAt
id:
custom_name: id
role:
custom_name: role
user_id:
custom_name: userId
custom_column_names:
created_at: createdAt
id: id
role: role
user_id: userId
custom_name: authUserRoles
custom_root_fields:

View File

@@ -11,14 +11,22 @@ configuration:
custom_name: createdAt
default_role:
custom_name: defaultRole
disabled:
custom_name: disabled
display_name:
custom_name: displayName
email:
custom_name: email
email_verified:
custom_name: emailVerified
id:
custom_name: id
is_anonymous:
custom_name: isAnonymous
last_seen:
custom_name: lastSeen
locale:
custom_name: locale
new_email:
custom_name: newEmail
otp_hash:
@@ -33,6 +41,8 @@ configuration:
custom_name: phoneNumber
phone_number_verified:
custom_name: phoneNumberVerified
ticket:
custom_name: ticket
ticket_expires_at:
custom_name: ticketExpiresAt
totp_secret:
@@ -46,10 +56,14 @@ configuration:
avatar_url: avatarUrl
created_at: createdAt
default_role: defaultRole
disabled: disabled
display_name: displayName
email: email
email_verified: emailVerified
id: id
is_anonymous: isAnonymous
last_seen: lastSeen
locale: locale
new_email: newEmail
otp_hash: otpHash
otp_hash_expires_at: otpHashExpiresAt
@@ -57,6 +71,7 @@ configuration:
password_hash: passwordHash
phone_number: phoneNumber
phone_number_verified: phoneNumberVerified
ticket: ticket
ticket_expires_at: ticketExpiresAt
totp_secret: totpSecret
updated_at: updatedAt
@@ -77,13 +92,6 @@ object_relationships:
using:
foreign_key_constraint_on: default_role
array_relationships:
- name: authenticators
using:
foreign_key_constraint_on:
column: user_id
table:
name: user_authenticators
schema: auth
- name: refreshTokens
using:
foreign_key_constraint_on:
@@ -98,6 +106,13 @@ array_relationships:
table:
name: user_roles
schema: auth
- name: securityKeys
using:
foreign_key_constraint_on:
column: user_id
table:
name: user_security_keys
schema: auth
- name: userProviders
using:
foreign_key_constraint_on:
@@ -106,12 +121,17 @@ array_relationships:
name: user_providers
schema: auth
select_permissions:
- role: public
permission:
columns:
- display_name
- id
filter: {}
limit: 0
- role: user
permission:
columns:
- avatar_url
- display_name
- id
filter:
id:
_eq: X-Hasura-User-Id
filter: {}
limit: 0

View File

@@ -1,17 +0,0 @@
table:
name: customers
schema: public
insert_permissions:
- role: public
permission:
check: {}
columns:
- name
select_permissions:
- role: public
permission:
columns:
- id
- name
- created_at
filter: {}

View File

@@ -1,80 +0,0 @@
table:
name: doc_links
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
doc_id:
custom_name: docId
download_allowed:
custom_name: downloadAllowed
is_active:
custom_name: isActive
require_email_to_view:
custom_name: requireEmailToView
updated_at:
custom_name: updatedAt
custom_column_names:
created_at: createdAt
doc_id: docId
download_allowed: downloadAllowed
is_active: isActive
require_email_to_view: requireEmailToView
updated_at: updatedAt
custom_root_fields:
insert: insertDocLinks
insert_one: insertDocLink
select: docLinks
select_by_pk: docLink
object_relationships:
- name: doc
using:
foreign_key_constraint_on: doc_id
array_relationships:
- name: docVisits
using:
foreign_key_constraint_on:
column: doc_link_id
table:
name: doc_visits
schema: public
insert_permissions:
- permission:
check:
doc:
user_id:
_eq: X-Hasura-User-Id
columns:
- doc_id
- download_allowed
- is_active
- passcode
- require_email_to_view
role: user
select_permissions:
- permission:
columns:
- download_allowed
- id
- is_active
- passcode
- require_email_to_view
filter: {}
limit: 0
role: public
- permission:
columns:
- download_allowed
- is_active
- require_email_to_view
- passcode
- created_at
- updated_at
- doc_id
- id
filter:
doc:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -1,35 +0,0 @@
table:
name: doc_visits
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
doc_link_id:
custom_name: docLinkId
updated_at:
custom_name: updatedAt
custom_column_names:
created_at: createdAt
doc_link_id: docLinkId
updated_at: updatedAt
custom_root_fields: {}
object_relationships:
- name: docLink
using:
foreign_key_constraint_on: doc_link_id
select_permissions:
- permission:
allow_aggregations: true
columns:
- email
- created_at
- updated_at
- doc_link_id
- id
filter:
docLink:
doc:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -1,72 +0,0 @@
table:
name: docs
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
file_id:
custom_name: fileId
updated_at:
custom_name: updatedAt
user_id:
custom_name: userId
custom_column_names:
created_at: createdAt
file_id: fileId
updated_at: updatedAt
user_id: userId
custom_root_fields:
delete: deleteDocs
delete_by_pk: DeleteDoc
insert: insertDocs
insert_one: insertDoc
select: docs
select_aggregate: docsAggregate
select_by_pk: doc
update: updateDocs
update_by_pk: updateDoc
object_relationships:
- name: file
using:
foreign_key_constraint_on: file_id
- name: user
using:
foreign_key_constraint_on: user_id
array_relationships:
- name: docLinks
using:
foreign_key_constraint_on:
column: doc_id
table:
name: doc_links
schema: public
insert_permissions:
- permission:
check: {}
columns:
- file_id
- name
set:
user_id: x-hasura-user-id
role: user
select_permissions:
- permission:
columns:
- file_id
- id
filter: {}
limit: 0
role: public
- permission:
columns:
- name
- created_at
- updated_at
- file_id
- id
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -9,6 +9,8 @@ configuration:
custom_name: createdAt
download_expiration:
custom_name: downloadExpiration
id:
custom_name: id
max_upload_file_size:
custom_name: maxUploadFileSize
min_upload_file_size:
@@ -21,6 +23,7 @@ configuration:
cache_control: cacheControl
created_at: createdAt
download_expiration: downloadExpiration
id: id
max_upload_file_size: maxUploadFileSize
min_upload_file_size: minUploadFileSize
presigned_urls_enabled: presignedUrlsEnabled

View File

@@ -9,10 +9,14 @@ configuration:
custom_name: createdAt
etag:
custom_name: etag
id:
custom_name: id
is_uploaded:
custom_name: isUploaded
mime_type:
custom_name: mimeType
name:
custom_name: name
size:
custom_name: size
updated_at:
@@ -23,8 +27,10 @@ configuration:
bucket_id: bucketId
created_at: createdAt
etag: etag
id: id
is_uploaded: isUploaded
mime_type: mimeType
name: name
size: size
updated_at: updatedAt
uploaded_by_user_id: uploadedByUserId

View File

@@ -2,10 +2,10 @@
- "!include auth_providers.yaml"
- "!include auth_refresh_tokens.yaml"
- "!include auth_roles.yaml"
- "!include auth_user_authenticators.yaml"
- "!include auth_user_providers.yaml"
- "!include auth_user_roles.yaml"
- "!include auth_user_security_keys.yaml"
- "!include auth_users.yaml"
- "!include public_customers.yaml"
- "!include public_tasks.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"

View File

@@ -1 +0,0 @@
CREATE TABLE "public"."customers" ("id" serial NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "name" text NOT NULL, PRIMARY KEY ("id") );

View File

@@ -2,16 +2,8 @@
"name": "@nhost-examples/codegen-react-apollo",
"version": "0.1.5",
"private": true,
"dependencies": {
"@apollo/client": "^3.6.9",
"@nhost/react": "*",
"@nhost/react-apollo": "*",
"graphql": "15.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"codegen": "graphql-codegen",
"dev": "vite",
"build": "vite build",
"preview": "vite preview --host localhost --port 3000"
@@ -22,28 +14,28 @@
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@apollo/client": "^3.7.3",
"@nhost/react": "*",
"@nhost/react-apollo": "*",
"clsx": "^1.2.1",
"graphql": "15.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.12.0",
"@graphql-codegen/typescript-operations": "^2.5.3",
"@graphql-codegen/typescript-react-apollo": "^3.3.3",
"@types/node": "^16.11.7",
"@graphql-codegen/cli": "^2.14.1",
"@graphql-codegen/client-preset": "^1.1.5",
"@graphql-typed-document-node/core": "^3.1.1",
"@tailwindcss/forms": "^0.5.3",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"eslint": "^8.23.0",
"eslint-config-react-app": "^7.0.1",
"typescript": "^4.8.2",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.1",
"typescript": "^4.6.4",
"vite": "^4.0.2"
}
}

View File

@@ -1,22 +1,40 @@
import { NhostProvider } from '@nhost/react'
import React from 'react'
import { NhostProvider, SignedIn, SignedOut } from '@nhost/react'
import { NhostApolloProvider } from '@nhost/react-apollo'
import { Customers } from './components/customers'
import { NewCustomer } from './components/new-customer'
import { Tasks } from './components/Tasks'
import { SignIn } from './components/SignIn'
import { nhost } from './utils/nhost'
import './index.css'
function App() {
return (
<NhostProvider nhost={nhost}>
<NhostApolloProvider nhost={nhost}>
<div>
<h1>GraphQL Code Generator example with React and Apollo</h1>
<div>
<NewCustomer />
<SignedIn>
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 mt-6 text-gray-200">
<div className="flex justify-between pb-4 mb-4 border-b border-gray-700">
<div className="uppercase font-semibold">Todo App</div>
<div>
<button
onClick={() => nhost.auth.signOut()}
type="submit"
className="rounded-sm border border-transparent px-3 py-2 text-sm font-medium leading-4 bg-slate-100 hover:bg-slate-200 text-gray-800 shadow-sm hover:focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
>
Sign Out
</button>
</div>
</div>
<Tasks />
</div>
<div>
<Customers />
</SignedIn>
<SignedOut>
<div className="h-full">
<SignIn />
</div>
</div>
</SignedOut>
</NhostApolloProvider>
</NhostProvider>
)

View File

@@ -40,7 +40,7 @@ export function SignIn() {
<div>
<button
type="submit"
className="rounded-sm border border-transparent bg-blue-600 px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
className="rounded-sm border border-transparent px-3 py-2 text-sm font-medium leading-4 bg-slate-100 hover:bg-slate-200 text-gray-800 shadow-sm hover:focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
>
Send Magic Link
</button>

View File

@@ -0,0 +1,165 @@
import clsx from 'clsx'
import React, { useState } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import { graphql } from '../gql/gql'
const GET_TASKS = graphql(`
query GetTasks {
tasks(order_by: { createdAt: desc }) {
id
name
done
}
}
`)
const INSERT_TASK = graphql(`
mutation InsertTask($task: tasks_insert_input!) {
insertTasks(objects: [$task]) {
affected_rows
returning {
id
name
}
}
}
`)
const UPDATE_TASK = graphql(`
mutation UpdateTask($id: uuid!, $task: tasks_set_input!) {
updateTask(pk_columns: { id: $id }, _set: $task) {
id
name
done
}
}
`)
const DELETE_TASK = graphql(`
mutation DeleteTask($id: uuid!) {
deleteTask(id: $id) {
id
}
}
`)
export function Tasks() {
const [name, setName] = useState('')
const { data, loading } = useQuery(GET_TASKS)
const [insertTask, { loading: insertPostIsLoading }] = useMutation(INSERT_TASK)
const [deleteTask] = useMutation(DELETE_TASK)
const [updateTask] = useMutation(UPDATE_TASK)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!name) {
return
}
await insertTask({
variables: {
task: {
name
}
},
refetchQueries: ['GetTasks']
})
setName('')
}
if (loading) {
return <div>Loading...</div>
}
if (!data) {
return <div>No data</div>
}
const { tasks } = data
return (
<div className="space-y-5">
<div>
<div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Todo
</label>
<div className="mt-1">
<input
type="text"
placeholder="Todo"
value={name}
onChange={(e) => setName(e.target.value)}
className="bg-gray-800 border-gray-600 text-gray-100 block w-full rounded-sm shadow-sm focus:shadow-md sm:text-sm"
/>
</div>
</div>
<div>
<button
type="submit"
className="rounded-sm border border-transparent px-3 py-2 text-sm font-medium leading-4 bg-slate-100 hover:bg-slate-200 text-gray-800 shadow-sm hover:focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
disabled={insertPostIsLoading}
>
Add
</button>
</div>
</form>
</div>
</div>
<div>
{tasks.map((task) => {
const style = clsx('p-2', {
'line-through text-green-600': task.done
})
return (
<div
key={task.id}
className="flex justify-between transition-all duration-300 ease-in-out"
>
<div className={style}>{task.name}</div>
<div className="flex space-x-4">
<button
onClick={() => {
updateTask({
variables: {
id: task.id,
task: {
done: !task.done
}
}
})
}}
className="cursor-pointer p-2 hover:bg-gray-900 rounded-sm transition-all duration-100 ease-in-out"
>
{task.done ? 'Not Done' : 'Done'}
</button>
<button
onClick={() => {
deleteTask({
variables: {
id: task.id
},
refetchQueries: ['GetTasks']
})
}}
className="cursor-pointer p-2 hover:bg-gray-900 rounded-sm transition-all duration-100 ease-in-out"
>
Delete
</button>
</div>
</div>
)
})}
</div>
</div>
)
}

View File

@@ -1,26 +0,0 @@
import { useGetCustomersQuery } from '../utils/__generated__/graphql'
export function Customers() {
const { data, loading, error } = useGetCustomersQuery()
if (loading || !data) {
return <div>Loading</div>
}
if (error) {
return <div>Error</div>
}
const { customers } = data
return (
<div>
<h2>Customers</h2>
<ul>
{customers.map((customer) => (
<li key={customer.id}>{customer.name}</li>
))}
</ul>
</div>
)
}

View File

@@ -1,55 +0,0 @@
import { useState } from 'react'
import { refetchGetCustomersQuery, useInsertCustomerMutation } from '../utils/__generated__/graphql'
export function NewCustomer() {
const [name, setName] = useState('')
const [insertCustomer, { loading, error }] = useInsertCustomerMutation({
refetchQueries: [refetchGetCustomersQuery()]
})
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
try {
await insertCustomer({
variables: {
customer: {
name
}
}
})
} catch (error) {
return console.error(error)
}
setName('')
alert('Customer added!')
}
return (
<div>
<h2>New Customer</h2>
<div>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
name="name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
{error && <div>Error: {error.message}</div>}
<div>
<button type="submit" disabled={loading}>
Add
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -1,12 +0,0 @@
query GetCustomers {
customers {
id
name
}
}
mutation InsertCustomer($customer: customers_insert_input!) {
insert_customers_one(object: $customer) {
id
}
}

View File

@@ -1,13 +1,7 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
html {
background: #000;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,11 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css?inline'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -2,6 +2,4 @@ import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css?inline'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)

View File

@@ -1,7 +1,8 @@
import { NhostClient } from '@nhost/react'
const nhost = new NhostClient({
subdomain: 'localhost:1337'
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || 'localhost',
region: import.meta.env.VITE_NHOST_REGION
})
export { nhost }

View File

@@ -6,8 +6,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
optimizeDeps: {
include: ['react/jsx-runtime'],
// * Shim: do not optimize @nhost/react when running this example in the Nhost monorepo
exclude: process.env.PNPM_PACKAGE_NAME === 'nhost-root' ? ['@nhost/react'] : []
exclude: ['@nhost/react']
},
plugins: [react()]
})

View File

@@ -1,11 +1,12 @@
# GraphQL Code Generator Example with React and React Query
This is an example repo for how to use GraphQL Code Generator together with:
Todo app to show how to use:
- [TypeScript](https://www.typescriptlang.org/)
- [Nhost](https://nhost.io/)
- [React](https://reactjs.org/)
- [TypeScript](https://www.typescriptlang.org/)
- [GraphQL Code Generator](https://the-guild.dev/graphql/codegen)
- [React Query](https://tanstack.com/query/v4/)
- [Nhost](http://nhost.io/)
This repo is a reference repo for the blog post: [How to use GraphQL Code Generator with React and React Query](https://nhost.io/blog/how-to-use-graphql-code-generator-with-react-query).
@@ -13,42 +14,42 @@ This repo is a reference repo for the blog post: [How to use GraphQL Code Genera
1. Clone the repository
```
```sh
git clone https://github.com/nhost/nhost
cd nhost
```
2. Install and build dependencies
```
```sh
pnpm install
pnpm build
pnpm run build
```
3. Go to the Codegen React Query example folder
3. Go to the example folder
```
```sh
cd examples/codegen-react-query
```
4. Terminal 1: Start Nhost
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
```sh
nhost up
nhost up -d
```
5. Terminal 2: Run GraphQL Codegen
5. Terminal 2: Start the React application
```sh
pnpm run dev
```
## GraphQL Code Generators
To re-run the GraphQL Code Generators, run the following:
```
pnpm codegen -w
```
> `-w` runs [codegen in watch mode](https://www.the-guild.dev/graphql/codegen/docs/getting-started/development-workflow#watch-mode).
6. Terminal 3: Start the React application
```sh
pnpm dev
```

View File

@@ -0,0 +1,23 @@
import { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: [
{
'http://localhost:1337/v1/graphql': {
headers: {
'x-hasura-admin-secret': 'nhost-admin-secret'
}
}
}
],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/gql/': {
documents: ['src/**/*.tsx'],
preset: 'client',
plugins: []
}
}
}
export default config

View File

@@ -2,15 +2,3 @@ schema:
- http://localhost:1337/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
generates:
src/utils/__generated__/graphql.ts:
documents:
- 'src/**/*.graphql'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-query'
config:
fetcher:
func: '../graphql-fetcher#fetchData'
isReactHook: false

View File

@@ -9,6 +9,6 @@
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,14 +1,25 @@
metadata_directory: metadata
services:
hasura:
image: hasura/graphql-engine:v2.15.2
environment:
hasura_graphql_enable_remote_schema_permissions: false
image: hasura/graphql-engine:v2.15.2
minio:
environment:
minio_root_password: minioaccesskey123123
minio_root_user: minioaccesskey123123
postgres:
environment:
postgres_password: postgres
postgres_user: postgres
auth:
image: nhost/hasura-auth:0.16.2
image: nhost/hasura-auth:0.16.1
storage:
image: nhost/hasura-storage:0.3.0
auth:
webauthn:
enabled: true
rp_name: URQL
access_control:
email:
allowed_email_domains: ''
@@ -18,13 +29,13 @@ auth:
url:
allowed_redirect_urls: ''
anonymous_users_enabled: false
client_url: http://localhost:3000
client_url: http://localhost:5173
disable_new_users: false
email:
enabled: false
passwordless:
enabled: false
signin_email_verified_required: true
enabled: true
signin_email_verified_required: false
template_fetch_url: ''
gravatar:
default: ''
@@ -117,11 +128,7 @@ auth:
secure: false
sender: hasura-auth@example.com
user: user
token:
access:
expires_in: 900
refresh:
expires_in: 43200
access_token_expires_in: 315
user:
allowed_roles: user,me
default_allowed_roles: user,me

View File

@@ -0,0 +1 @@
Your code is ${code}.

View File

@@ -0,0 +1 @@
Votre code est ${code}.

View File

@@ -9,6 +9,6 @@
connection_lifetime: 600
idle_timeout: 180
max_connections: 50
retries: 1
retries: 20
use_prepared_statements: true
tables: "!include default/tables/tables.yaml"

View File

@@ -2,8 +2,14 @@ table:
name: provider_requests
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
id:
custom_name: id
options:
custom_name: options
custom_column_names:
id: id
options: options
custom_name: authProviderRequests
custom_root_fields:
delete: deleteAuthProviderRequests

View File

@@ -2,8 +2,11 @@ table:
name: providers
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
id:
custom_name: id
custom_column_names:
id: id
custom_name: authProviders
custom_root_fields:
delete: deleteAuthProviders

View File

@@ -2,8 +2,11 @@ table:
name: roles
schema: auth
configuration:
column_config: {}
custom_column_names: {}
column_config:
role:
custom_name: role
custom_column_names:
role: role
custom_name: authRoles
custom_root_fields:
delete: deleteAuthRoles

View File

@@ -1,30 +0,0 @@
table:
name: user_authenticators
schema: auth
configuration:
column_config:
credential_id:
custom_name: credentialId
credential_public_key:
custom_name: credentialPublicKey
user_id:
custom_name: userId
custom_column_names:
credential_id: credentialId
credential_public_key: credentialPublicKey
user_id: userId
custom_name: authUserAuthenticators
custom_root_fields:
delete: deleteAuthUserAuthenticators
delete_by_pk: deleteAuthUserAuthenticator
insert: insertAuthUserAuthenticators
insert_one: insertAuthUserAuthenticator
select: authUserAuthenticators
select_aggregate: authUserAuthenticatorsAggregate
select_by_pk: authUserAuthenticator
update: updateAuthUserAuthenticators
update_by_pk: updateAuthUserAuthenticator
object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id

View File

@@ -7,6 +7,8 @@ configuration:
custom_name: accessToken
created_at:
custom_name: createdAt
id:
custom_name: id
provider_id:
custom_name: providerId
provider_user_id:
@@ -20,6 +22,7 @@ configuration:
custom_column_names:
access_token: accessToken
created_at: createdAt
id: id
provider_id: providerId
provider_user_id: providerUserId
refresh_token: refreshToken

View File

@@ -5,10 +5,16 @@ configuration:
column_config:
created_at:
custom_name: createdAt
id:
custom_name: id
role:
custom_name: role
user_id:
custom_name: userId
custom_column_names:
created_at: createdAt
id: id
role: role
user_id: userId
custom_name: authUserRoles
custom_root_fields:

View File

@@ -0,0 +1,33 @@
table:
name: user_security_keys
schema: auth
configuration:
column_config:
credential_id:
custom_name: credentialId
credential_public_key:
custom_name: credentialPublicKey
id:
custom_name: id
user_id:
custom_name: userId
custom_column_names:
credential_id: credentialId
credential_public_key: credentialPublicKey
id: id
user_id: userId
custom_name: authUserSecurityKeys
custom_root_fields:
delete: deleteAuthUserSecurityKeys
delete_by_pk: deleteAuthUserSecurityKey
insert: insertAuthUserSecurityKeys
insert_one: insertAuthUserSecurityKey
select: authUserSecurityKeys
select_aggregate: authUserSecurityKeysAggregate
select_by_pk: authUserSecurityKey
update: updateAuthUserSecurityKeys
update_by_pk: updateAuthUserSecurityKey
object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id

View File

@@ -11,14 +11,22 @@ configuration:
custom_name: createdAt
default_role:
custom_name: defaultRole
disabled:
custom_name: disabled
display_name:
custom_name: displayName
email:
custom_name: email
email_verified:
custom_name: emailVerified
id:
custom_name: id
is_anonymous:
custom_name: isAnonymous
last_seen:
custom_name: lastSeen
locale:
custom_name: locale
new_email:
custom_name: newEmail
otp_hash:
@@ -33,6 +41,8 @@ configuration:
custom_name: phoneNumber
phone_number_verified:
custom_name: phoneNumberVerified
ticket:
custom_name: ticket
ticket_expires_at:
custom_name: ticketExpiresAt
totp_secret:
@@ -46,10 +56,14 @@ configuration:
avatar_url: avatarUrl
created_at: createdAt
default_role: defaultRole
disabled: disabled
display_name: displayName
email: email
email_verified: emailVerified
id: id
is_anonymous: isAnonymous
last_seen: lastSeen
locale: locale
new_email: newEmail
otp_hash: otpHash
otp_hash_expires_at: otpHashExpiresAt
@@ -57,6 +71,7 @@ configuration:
password_hash: passwordHash
phone_number: phoneNumber
phone_number_verified: phoneNumberVerified
ticket: ticket
ticket_expires_at: ticketExpiresAt
totp_secret: totpSecret
updated_at: updatedAt
@@ -77,13 +92,6 @@ object_relationships:
using:
foreign_key_constraint_on: default_role
array_relationships:
- name: authenticators
using:
foreign_key_constraint_on:
column: user_id
table:
name: user_authenticators
schema: auth
- name: refreshTokens
using:
foreign_key_constraint_on:
@@ -98,6 +106,13 @@ array_relationships:
table:
name: user_roles
schema: auth
- name: securityKeys
using:
foreign_key_constraint_on:
column: user_id
table:
name: user_security_keys
schema: auth
- name: userProviders
using:
foreign_key_constraint_on:
@@ -106,12 +121,17 @@ array_relationships:
name: user_providers
schema: auth
select_permissions:
- role: public
permission:
columns:
- display_name
- id
filter: {}
limit: 0
- role: user
permission:
columns:
- avatar_url
- display_name
- id
filter:
id:
_eq: X-Hasura-User-Id
filter: {}
limit: 0

View File

@@ -1,17 +0,0 @@
table:
name: customers
schema: public
insert_permissions:
- role: public
permission:
check: {}
columns:
- name
select_permissions:
- role: public
permission:
columns:
- id
- name
- created_at
filter: {}

View File

@@ -1,80 +0,0 @@
table:
name: doc_links
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
doc_id:
custom_name: docId
download_allowed:
custom_name: downloadAllowed
is_active:
custom_name: isActive
require_email_to_view:
custom_name: requireEmailToView
updated_at:
custom_name: updatedAt
custom_column_names:
created_at: createdAt
doc_id: docId
download_allowed: downloadAllowed
is_active: isActive
require_email_to_view: requireEmailToView
updated_at: updatedAt
custom_root_fields:
insert: insertDocLinks
insert_one: insertDocLink
select: docLinks
select_by_pk: docLink
object_relationships:
- name: doc
using:
foreign_key_constraint_on: doc_id
array_relationships:
- name: docVisits
using:
foreign_key_constraint_on:
column: doc_link_id
table:
name: doc_visits
schema: public
insert_permissions:
- permission:
check:
doc:
user_id:
_eq: X-Hasura-User-Id
columns:
- doc_id
- download_allowed
- is_active
- passcode
- require_email_to_view
role: user
select_permissions:
- permission:
columns:
- download_allowed
- id
- is_active
- passcode
- require_email_to_view
filter: {}
limit: 0
role: public
- permission:
columns:
- download_allowed
- is_active
- require_email_to_view
- passcode
- created_at
- updated_at
- doc_id
- id
filter:
doc:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -1,35 +0,0 @@
table:
name: doc_visits
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
doc_link_id:
custom_name: docLinkId
updated_at:
custom_name: updatedAt
custom_column_names:
created_at: createdAt
doc_link_id: docLinkId
updated_at: updatedAt
custom_root_fields: {}
object_relationships:
- name: docLink
using:
foreign_key_constraint_on: doc_link_id
select_permissions:
- permission:
allow_aggregations: true
columns:
- email
- created_at
- updated_at
- doc_link_id
- id
filter:
docLink:
doc:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -1,72 +0,0 @@
table:
name: docs
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
file_id:
custom_name: fileId
updated_at:
custom_name: updatedAt
user_id:
custom_name: userId
custom_column_names:
created_at: createdAt
file_id: fileId
updated_at: updatedAt
user_id: userId
custom_root_fields:
delete: deleteDocs
delete_by_pk: DeleteDoc
insert: insertDocs
insert_one: insertDoc
select: docs
select_aggregate: docsAggregate
select_by_pk: doc
update: updateDocs
update_by_pk: updateDoc
object_relationships:
- name: file
using:
foreign_key_constraint_on: file_id
- name: user
using:
foreign_key_constraint_on: user_id
array_relationships:
- name: docLinks
using:
foreign_key_constraint_on:
column: doc_id
table:
name: doc_links
schema: public
insert_permissions:
- permission:
check: {}
columns:
- file_id
- name
set:
user_id: x-hasura-user-id
role: user
select_permissions:
- permission:
columns:
- file_id
- id
filter: {}
limit: 0
role: public
- permission:
columns:
- name
- created_at
- updated_at
- file_id
- id
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
role: user

View File

@@ -0,0 +1,66 @@
table:
name: posts
schema: public
configuration:
column_config: {}
custom_column_names: {}
custom_root_fields:
delete: deletePosts
delete_by_pk: deletePost
insert: insertPosts
insert_one: insertPost
update: updatePosts
update_by_pk: updatePost
insert_permissions:
- role: user
permission:
check: {}
set:
user_id: x-hasura-user-id
columns:
- is_public
- title
select_permissions:
- role: public
permission:
columns:
- is_public
- title
- created_at
- updated_at
- id
- user_id
filter:
is_public:
_eq: true
- role: user
permission:
columns:
- is_public
- title
- created_at
- updated_at
- id
- user_id
filter:
_or:
- is_public:
_eq: true
- user_id:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- is_public
- title
filter:
user_id:
_eq: X-Hasura-User-Id
check: null
delete_permissions:
- role: user
permission:
filter:
user_id:
_eq: X-Hasura-User-Id

View File

@@ -0,0 +1,65 @@
table:
name: tasks
schema: public
configuration:
column_config:
created_at:
custom_name: createdAt
updated_at:
custom_name: updatedAt
custom_column_names:
created_at: createdAt
updated_at: updatedAt
custom_root_fields:
delete: deleteTasks
delete_by_pk: deleteTask
insert: insertTasks
insert_one: insertTask
select: tasks
select_aggregate: tasksAggregate
select_by_pk: task
select_stream: tasksStream
update: updateTasks
update_by_pk: updateTask
update_many: updateManyTasks
object_relationships:
- name: user
using:
foreign_key_constraint_on: user_id
insert_permissions:
- role: user
permission:
check: {}
set:
user_id: x-hasura-user-id
columns:
- name
select_permissions:
- role: user
permission:
columns:
- done
- name
- created_at
- updated_at
- id
- user_id
filter:
user_id:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- done
- name
filter:
user_id:
_eq: X-Hasura-User-Id
check: null
delete_permissions:
- role: user
permission:
filter:
user_id:
_eq: X-Hasura-User-Id

View File

@@ -9,6 +9,8 @@ configuration:
custom_name: createdAt
download_expiration:
custom_name: downloadExpiration
id:
custom_name: id
max_upload_file_size:
custom_name: maxUploadFileSize
min_upload_file_size:
@@ -21,6 +23,7 @@ configuration:
cache_control: cacheControl
created_at: createdAt
download_expiration: downloadExpiration
id: id
max_upload_file_size: maxUploadFileSize
min_upload_file_size: minUploadFileSize
presigned_urls_enabled: presignedUrlsEnabled

View File

@@ -9,10 +9,14 @@ configuration:
custom_name: createdAt
etag:
custom_name: etag
id:
custom_name: id
is_uploaded:
custom_name: isUploaded
mime_type:
custom_name: mimeType
name:
custom_name: name
size:
custom_name: size
updated_at:
@@ -23,8 +27,10 @@ configuration:
bucket_id: bucketId
created_at: createdAt
etag: etag
id: id
is_uploaded: isUploaded
mime_type: mimeType
name: name
size: size
updated_at: updatedAt
uploaded_by_user_id: uploadedByUserId

View File

@@ -2,10 +2,10 @@
- "!include auth_providers.yaml"
- "!include auth_refresh_tokens.yaml"
- "!include auth_roles.yaml"
- "!include auth_user_authenticators.yaml"
- "!include auth_user_providers.yaml"
- "!include auth_user_roles.yaml"
- "!include auth_user_security_keys.yaml"
- "!include auth_users.yaml"
- "!include public_customers.yaml"
- "!include public_tasks.yaml"
- "!include storage_buckets.yaml"
- "!include storage_files.yaml"

View File

@@ -1 +0,0 @@
CREATE TABLE "public"."customers" ("id" serial NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "name" text NOT NULL, PRIMARY KEY ("id") );

View File

@@ -0,0 +1 @@
DROP TABLE "public"."tasks";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."tasks" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "name" text NOT NULL, "done" bool NOT NULL DEFAULT false, "user_id" uuid NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON UPDATE restrict ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_tasks_updated_at"
BEFORE UPDATE ON "public"."tasks"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_tasks_updated_at" ON "public"."tasks"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -2,16 +2,8 @@
"name": "@nhost-examples/codegen-react-query",
"version": "0.1.5",
"private": true,
"dependencies": {
"@nhost/react": "*",
"@tanstack/react-query": "^4.2.3",
"@tanstack/react-query-devtools": "^4.2.3",
"graphql": "15.7.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"codegen": "graphql-codegen",
"dev": "vite",
"build": "vite build",
"preview": "vite preview --host localhost --port 3000"
@@ -22,26 +14,28 @@
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@nhost/react": "*",
"@tanstack/react-query": "^4.2.3",
"@tanstack/react-query-devtools": "^4.2.3",
"clsx": "^1.2.1",
"graphql": "15.7.2",
"graphql-request": "^5.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^2.12.0",
"@graphql-codegen/typescript-operations": "^2.5.3",
"@graphql-codegen/typescript-react-query": "^4.0.1",
"@types/node": "^16.11.7",
"@graphql-codegen/cli": "^2.14.1",
"@graphql-codegen/client-preset": "^1.1.5",
"@graphql-typed-document-node/core": "^3.1.1",
"@tailwindcss/forms": "^0.5.3",
"@types/node": "^16.11.56",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"autoprefixer": "^10.4.13",
"eslint": "^8.23.0",
"postcss": "^8.4.20",
"tailwindcss": "^3.2.4",
"typescript": "^4.8.2",
"vite": "^4.0.2"
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,25 +1,41 @@
import { NhostProvider } from '@nhost/react'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { Customers } from './components/customers'
import { NewCustomer } from './components/new-customer'
import React from 'react'
import { NhostProvider, SignedIn, SignedOut } from '@nhost/react'
import { Tasks } from './components/Tasks'
import { SignIn } from './components/SignIn'
import { nhost } from './utils/nhost'
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from './utils/react-query-client'
import './index.css'
function App() {
return (
<NhostProvider nhost={nhost}>
<QueryClientProvider client={queryClient}>
<div>
<h1>GraphQL Code Generator example with React and React Query</h1>
<div>
<NewCustomer />
<SignedIn>
<div className="mx-auto max-w-3xl px-4 sm:px-6 lg:px-8 mt-6 text-gray-200">
<div className="flex justify-between pb-4 mb-4 border-b border-gray-700">
<div className="uppercase font-semibold">Todo App</div>
<div>
<button
onClick={() => nhost.auth.signOut()}
type="submit"
className="rounded-sm border border-transparent px-3 py-2 text-sm font-medium leading-4 bg-slate-100 hover:bg-slate-200 text-gray-800 shadow-sm hover:focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
>
Sign Out
</button>
</div>
</div>
<Tasks />
</div>
<div>
<Customers />
</SignedIn>
<SignedOut>
<div className="h-full">
<SignIn />
</div>
</div>
<ReactQueryDevtools initialIsOpen={false} />
</SignedOut>
</QueryClientProvider>
</NhostProvider>
)

View File

@@ -0,0 +1,53 @@
import React, { useState } from 'react'
import { useSignInEmailPasswordless } from '@nhost/react'
export function SignIn() {
const [email, setEmail] = useState('')
const { signInEmailPasswordless, error } = useSignInEmailPasswordless()
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const { error } = await signInEmailPasswordless(email)
if (error) {
alert('Error signing in')
console.log(error)
return
}
alert('Magic Link Sent')
window.open('http://localhost:8025/', '_blank')
}
return (
<div className="my-4 max-w-xs mx-auto">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-500">
Email
</label>
<div className="mt-1">
<input
name="email"
type="email"
placeholder="you@example.com"
onChange={(e) => setEmail(e.target.value)}
id="email"
className="bg-gray-800 border-gray-600 text-gray-100 block w-full rounded-sm shadow-sm focus:shadow-md sm:text-sm"
/>
</div>
</div>
<div>
<button
type="submit"
className="rounded-sm border border-transparent px-3 py-2 text-sm font-medium leading-4 bg-slate-100 hover:bg-slate-200 text-gray-800 shadow-sm hover:focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 w-full "
>
Send Magic Link
</button>
</div>
{error && <div>{error.message}</div>}
</form>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More