Parcourir la source

WIP - Client side GraphQL [skip ci]

Nicolas Meienberger il y a 3 ans
Parent
commit
537cdcd811

+ 9 - 0
packages/dashboard/codegen.yml

@@ -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"

+ 10 - 1
packages/dashboard/package.json

@@ -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",

+ 11 - 0
packages/dashboard/src/core/apollo/client.ts

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

+ 14 - 0
packages/dashboard/src/core/apollo/links/errorLink.ts

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

+ 9 - 0
packages/dashboard/src/core/apollo/links/httpLink.ts

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

+ 9 - 0
packages/dashboard/src/core/apollo/links/index.ts

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

+ 258 - 0
packages/dashboard/src/generated/graphql.tsx

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

+ 7 - 0
packages/dashboard/src/graphql/mutations/login.graphql

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

+ 3 - 0
packages/dashboard/src/graphql/queries/isConfigured.graphql

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

+ 5 - 0
packages/dashboard/src/graphql/queries/me.graphql

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

+ 49 - 0
packages/dashboard/src/hooks/useCachedRessources.ts

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

+ 8 - 33
packages/dashboard/src/modules/Auth/containers/AuthWrapper.tsx

@@ -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 { 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]);
+  const user = useMeQuery();
+  const isConfigured = useConfiguredQuery();
+  const loading = user.loading || isConfigured.loading;
 
-  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 />;
   }
 

+ 8 - 5
packages/dashboard/src/modules/Auth/containers/Login.tsx

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

+ 15 - 5
packages/dashboard/src/pages/_app.tsx

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

Fichier diff supprimé car celui-ci est trop grand
+ 889 - 75
pnpm-lock.yaml


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff