WIP - Client side GraphQL [skip ci]

This commit is contained in:
Nicolas Meienberger 2022-06-20 22:57:27 +02:00
parent ce615a40f1
commit 537cdcd811
15 changed files with 2712 additions and 105 deletions

View file

@ -0,0 +1,9 @@
overwrite: true
schema: "http://localhost:3001/graphql"
documents: "src/graphql/**/*.graphql"
generates:
src/generated/graphql.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"

View file

@ -7,9 +7,11 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"gen": "graphql-codegen --config codegen.yml"
},
"dependencies": {
"@apollo/client": "^3.6.8",
"@chakra-ui/react": "^2.1.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
@ -19,6 +21,8 @@
"clsx": "^1.1.1",
"final-form": "^4.20.6",
"framer-motion": "^6",
"graphql": "^15.8.0",
"graphql-tag": "^2.12.6",
"immer": "^9.0.12",
"js-cookie": "^3.0.1",
"next": "12.1.6",
@ -35,11 +39,16 @@
"remark-mdx": "^2.1.1",
"swr": "^1.3.0",
"systeminformation": "^5.11.9",
"tslib": "^2.4.0",
"validator": "^13.7.0",
"zustand": "^3.7.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@graphql-codegen/cli": "^2.6.2",
"@graphql-codegen/typescript": "^2.5.1",
"@graphql-codegen/typescript-operations": "^2.4.2",
"@graphql-codegen/typescript-react-apollo": "^3.2.16",
"@types/js-cookie": "^3.0.2",
"@types/node": "17.0.31",
"@types/react": "18.0.8",

View file

@ -0,0 +1,11 @@
import { ApolloClient, from, InMemoryCache } from '@apollo/client';
import links from './links';
export const createApolloClient = async (ip: string): Promise<ApolloClient<any>> => {
const additiveLink = from([links.errorLink, links.httpLink(ip)]);
return new ApolloClient({
link: additiveLink,
cache: new InMemoryCache(),
});
};

View file

@ -0,0 +1,14 @@
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) => {
console.warn(`Error link [GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`);
});
if (networkError) {
console.warn(`Error link [Network error]: ${networkError}`);
}
});
export default errorLink;

View file

@ -0,0 +1,9 @@
import { HttpLink } from '@apollo/client';
const httpLink = (ip: string) =>
new HttpLink({
uri: `http://${ip}:3001/graphql`,
credentials: 'include',
});
export default httpLink;

View file

@ -0,0 +1,9 @@
import errorLink from './errorLink';
import httpLink from './httpLink';
const links = {
errorLink,
httpLink,
};
export default links;

View file

@ -0,0 +1,258 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
const defaultOptions = {} as const;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
DateTime: any;
/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSONObject: any;
};
export type App = {
__typename?: 'App';
createdAt: Scalars['DateTime'];
id: Scalars['String'];
lastOpened: Scalars['DateTime'];
numOpened: Scalars['Float'];
status: Scalars['String'];
updatedAt: Scalars['DateTime'];
};
export type AppConfig = {
__typename?: 'AppConfig';
author: Scalars['String'];
available: Scalars['Boolean'];
categories: Array<Scalars['String']>;
description: Scalars['String'];
form_fields: Array<FormField>;
id: Scalars['String'];
image: Scalars['String'];
installed: Scalars['Boolean'];
name: Scalars['String'];
port: Scalars['Float'];
short_desc: Scalars['String'];
source: Scalars['String'];
status: Scalars['String'];
url_suffix?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
};
export type AppInputType = {
form: Scalars['JSONObject'];
id: Scalars['String'];
};
export type FormField = {
__typename?: 'FormField';
env_variable: Scalars['String'];
hint?: Maybe<Scalars['String']>;
label: Scalars['String'];
max?: Maybe<Scalars['Float']>;
min?: Maybe<Scalars['Float']>;
required?: Maybe<Scalars['Boolean']>;
type: Scalars['String'];
};
export type ListAppsResonse = {
__typename?: 'ListAppsResonse';
apps: Array<AppConfig>;
total: Scalars['Float'];
};
export type Mutation = {
__typename?: 'Mutation';
installApp: App;
login: UserResponse;
logout: Scalars['Boolean'];
register: UserResponse;
startApp: App;
stopApp: App;
uninstallApp: App;
updateAppConfig: App;
};
export type MutationInstallAppArgs = {
input: AppInputType;
};
export type MutationLoginArgs = {
input: UsernamePasswordInput;
};
export type MutationRegisterArgs = {
input: UsernamePasswordInput;
};
export type MutationStartAppArgs = {
id: Scalars['String'];
};
export type MutationStopAppArgs = {
id: Scalars['String'];
};
export type MutationUninstallAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppConfigArgs = {
input: AppInputType;
};
export type Query = {
__typename?: 'Query';
getAppInfo: AppConfig;
installedApps: Array<App>;
isConfigured: Scalars['Boolean'];
listAppsInfo: ListAppsResonse;
me?: Maybe<User>;
};
export type QueryGetAppInfoArgs = {
id: Scalars['String'];
};
export type User = {
__typename?: 'User';
createdAt: Scalars['DateTime'];
id: Scalars['ID'];
updatedAt: Scalars['DateTime'];
username: Scalars['String'];
};
export type UserResponse = {
__typename?: 'UserResponse';
user?: Maybe<User>;
};
export type UsernamePasswordInput = {
password: Scalars['String'];
username: Scalars['String'];
};
export type LoginMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
export type MeQueryVariables = Exact<{ [key: string]: never }>;
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
export const LoginDocument = gql`
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
user {
id
}
}
}
`;
export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
/**
* __useLoginMutation__
*
* To run a mutation, you first call `useLoginMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useLoginMutation` 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 [loginMutation, { data, loading, error }] = useLoginMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useLoginMutation(baseOptions?: Apollo.MutationHookOptions<LoginMutation, LoginMutationVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, options);
}
export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
export const ConfiguredDocument = gql`
query Configured {
isConfigured
}
`;
/**
* __useConfiguredQuery__
*
* To run a query within a React component, call `useConfiguredQuery` and pass it any options that fit your needs.
* When your component renders, `useConfiguredQuery` 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 query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useConfiguredQuery({
* variables: {
* },
* });
*/
export function useConfiguredQuery(baseOptions?: Apollo.QueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
}
export function useConfiguredLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ConfiguredQuery, ConfiguredQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<ConfiguredQuery, ConfiguredQueryVariables>(ConfiguredDocument, options);
}
export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
export const MeDocument = gql`
query Me {
me {
id
}
}
`;
/**
* __useMeQuery__
*
* To run a query within a React component, call `useMeQuery` and pass it any options that fit your needs.
* When your component renders, `useMeQuery` 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 query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useMeQuery({
* variables: {
* },
* });
*/
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
}
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
}
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;

View file

@ -0,0 +1,7 @@
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
user {
id
}
}
}

View file

@ -0,0 +1,3 @@
query Configured {
isConfigured
}

View file

@ -0,0 +1,5 @@
query Me {
me {
id
}
}

View file

@ -0,0 +1,49 @@
import { ApolloClient } from '@apollo/client';
import axios from 'axios';
import * as React from 'react';
import useSWR, { BareFetcher } from 'swr';
import { createApolloClient } from '../core/apollo/client';
import { useSytemStore } from '../state/systemStore';
interface IReturnProps {
client?: ApolloClient<unknown>;
isLoadingComplete?: boolean;
}
const fetcher: BareFetcher<any> = (url: string) => {
return axios.get(url).then((res) => res.data);
};
export default function useCachedResources(): IReturnProps {
const { data } = useSWR('/api/ip', fetcher);
const { internalIp, setInternalIp } = useSytemStore();
const [isLoadingComplete, setLoadingComplete] = React.useState(false);
const [client, setClient] = React.useState<ApolloClient<unknown>>();
async function loadResourcesAndDataAsync(ip: string) {
try {
const restoredClient = await createApolloClient(ip);
setClient(restoredClient);
} catch (error) {
// We might want to provide this error information to an error reporting service
console.warn(error);
} finally {
setLoadingComplete(true);
}
}
React.useEffect(() => {
if (data?.ip && !internalIp) {
setInternalIp(data.ip);
}
}, [data?.ip, internalIp, setInternalIp]);
React.useEffect(() => {
if (internalIp) {
loadResourcesAndDataAsync(internalIp);
}
}, [internalIp]);
return { client, isLoadingComplete };
}

View file

@ -1,9 +1,6 @@
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import useSWR, { BareFetcher } from 'swr';
import React from 'react';
import LoadingScreen from '../../../components/LoadingScreen';
import { useAuthStore } from '../../../state/authStore';
import { useSytemStore } from '../../../state/systemStore';
import { useConfiguredQuery, useMeQuery } from '../../../generated/graphql';
import Login from './Login';
import Onboarding from './Onboarding';
@ -11,42 +8,20 @@ interface IProps {
children: React.ReactNode;
}
const fetcher: BareFetcher<any> = (url: string) => {
return axios.get(url).then((res) => res.data);
};
const AuthWrapper: React.FC<IProps> = ({ children }) => {
const [initialLoad, setInitialLoad] = useState(true);
const { configured, user, me, fetchConfigured } = useAuthStore();
const { internalIp, setInternalIp } = useSytemStore();
const user = useMeQuery();
const isConfigured = useConfiguredQuery();
const loading = user.loading || isConfigured.loading;
const { data } = useSWR('/api/ip', fetcher);
useEffect(() => {
const fetchUser = async () => {
await me();
await fetchConfigured();
setInitialLoad(false);
};
if (!user && internalIp) fetchUser();
}, [fetchConfigured, internalIp, me, user]);
useEffect(() => {
if (data?.ip && !internalIp) {
setInternalIp(data.ip);
}
}, [data?.ip, internalIp, setInternalIp]);
if (initialLoad && !user) {
if (loading && !user.data?.me) {
return <LoadingScreen />;
}
if (user) {
if (user.data?.me) {
return <>{children}</>;
}
if (!configured) {
if (!isConfigured?.data?.isConfigured) {
return <Onboarding />;
}

View file

@ -1,13 +1,14 @@
import { useToast } from '@chakra-ui/react';
import React from 'react';
import { useAuthStore } from '../../../state/authStore';
import React, { useState } from 'react';
import { useLoginMutation } from '../../../generated/graphql';
import AuthFormLayout from '../components/AuthFormLayout';
import LoginForm from '../components/LoginForm';
type FormValues = { email: string; password: string };
const Login: React.FC = () => {
const { me, login, loading } = useAuthStore();
const [login] = useLoginMutation({ refetchQueries: ['Me'] });
const [loading, setLoading] = useState(false);
const toast = useToast();
const handleError = (error: unknown) => {
@ -24,10 +25,12 @@ const Login: React.FC = () => {
const handleLogin = async (values: FormValues) => {
try {
await login(values.email, values.password);
await me();
setLoading(true);
await login({ variables: { input: { username: values.email, password: values.password } } });
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
};

View file

@ -5,14 +5,24 @@ import { ChakraProvider } from '@chakra-ui/react';
import type { AppProps } from 'next/app';
import { theme } from '../styles/theme';
import AuthWrapper from '../modules/Auth/containers/AuthWrapper';
import { ApolloProvider } from '@apollo/client';
import useCachedResources from '../hooks/useCachedRessources';
function MyApp({ Component, pageProps }: AppProps) {
const { client } = useCachedResources();
if (!client) {
return null;
}
return (
<ChakraProvider theme={theme}>
<AuthWrapper>
<Component {...pageProps} />
</AuthWrapper>
</ChakraProvider>
<ApolloProvider client={client}>
<ChakraProvider theme={theme}>
<AuthWrapper>
<Component {...pageProps} />
</AuthWrapper>
</ChakraProvider>
</ApolloProvider>
);
}

File diff suppressed because it is too large Load diff