瀏覽代碼

feat(admin-ui): Proof-of-concept with apollo-link-state

Michael Bromley 7 年之前
父節點
當前提交
a477d59a2e

+ 2 - 0
admin-ui/package.json

@@ -32,6 +32,8 @@
     "apollo-angular-link-http": "^1.1.0",
     "apollo-cache-inmemory": "^1.2.4",
     "apollo-client": "^2.3.4",
+    "apollo-link": "^1.2.2",
+    "apollo-link-state": "^0.4.1",
     "core-js": "^2.5.4",
     "graphql": "^0.13.2",
     "graphql-tag": "^2.9.2",

+ 5 - 3
admin-ui/src/app/app.component.ts

@@ -1,7 +1,8 @@
 import { Component, OnInit } from '@angular/core';
 import { Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { map, tap } from 'rxjs/operators';
 
+import { DataService } from './data/providers/data.service';
 import { StateStore } from './state/state-store.service';
 
 @Component({
@@ -12,11 +13,12 @@ import { StateStore } from './state/state-store.service';
 export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
 
-    constructor(private store: StateStore) {
+    constructor(private dataService: DataService) {
     }
 
     ngOnInit() {
-        this.loading$ = this.store.select(state => state.api.inFlightRequests).pipe(
+        this.loading$ = this.dataService.local.inFlightRequests().pipe(
+            tap(val => console.log('inFlightRequests:', val)),
             map(count => 0 < count),
         );
     }

+ 1 - 1
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { Observable } from 'rxjs';
 
-import { DataService } from '../../../core/providers/data/data.service';
+import { DataService } from '../../../data/providers/data.service';
 
 @Component({
     selector: 'vdr-product-detail',

+ 1 - 1
admin-ui/src/app/catalog/components/product-list/product-list.component.ts

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
 import { map, tap } from 'rxjs/operators';
 
 import { GetProductListQuery, GetProductListQueryVariables } from '../../../common/types/gql-generated-types';
-import { DataService } from '../../../core/providers/data/data.service';
+import { DataService } from '../../../data/providers/data.service';
 
 @Component({
     selector: 'vdr-products-list',

+ 3 - 29
admin-ui/src/app/core/core.module.ts

@@ -1,41 +1,23 @@
-import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
 import { NgModule } from '@angular/core';
-import { Apollo, APOLLO_OPTIONS, ApolloModule } from 'apollo-angular';
-import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { API_PATH } from '../../../../shared/shared-constants';
-import { API_URL } from '../app.config';
 import { SharedModule } from '../shared/shared.module';
-import { APOLLO_NGRX_CACHE, StateModule } from '../state/state.module';
+import { StateModule } from '../state/state.module';
 
+import { DataModule } from '../data/data.module';
 import { AppShellComponent } from './components/app-shell/app-shell.component';
 import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
-import { BaseDataService } from './providers/data/base-data.service';
-import { DataService } from './providers/data/data.service';
-import { DefaultInterceptor } from './providers/data/interceptor';
 import { AuthGuard } from './providers/guard/auth.guard';
 import { LocalStorageService } from './providers/local-storage/local-storage.service';
 import { NotificationService } from './providers/notification/notification.service';
 import { OverlayHostService } from './providers/overlay-host/overlay-host.service';
 
-export function createApollo(httpLink: HttpLink, ngrxCache: InMemoryCache) {
-  return {
-    link: httpLink.create({ uri: `${API_URL}/${API_PATH}` }),
-    cache: ngrxCache,
-  };
-}
-
 @NgModule({
     imports: [
+        DataModule,
         SharedModule,
-        HttpClientModule,
-        ApolloModule,
-        HttpLinkModule,
         StateModule,
     ],
     exports: [
@@ -43,15 +25,7 @@ export function createApollo(httpLink: HttpLink, ngrxCache: InMemoryCache) {
         OverlayHostComponent,
     ],
     providers: [
-        {
-            provide: APOLLO_OPTIONS,
-            useFactory: createApollo,
-            deps: [HttpLink, APOLLO_NGRX_CACHE],
-        },
-        { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
-        BaseDataService,
         LocalStorageService,
-        DataService,
         AuthGuard,
         OverlayHostService,
         NotificationService,

+ 98 - 0
admin-ui/src/app/data/data.module.ts

@@ -0,0 +1,98 @@
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { ApolloLink } from 'apollo-link';
+
+import { APOLLO_OPTIONS, ApolloModule } from 'apollo-angular';
+import { withClientState } from 'apollo-link-state';
+import gql from 'graphql-tag';
+import { API_PATH } from '../../../../shared/shared-constants';
+import { API_URL } from '../app.config';
+import { BaseDataService } from './providers/base-data.service';
+import { DataService } from './providers/data.service';
+import { DefaultInterceptor } from './providers/interceptor';
+
+// This is the same cache you pass into new ApolloClient
+const apolloCache = new InMemoryCache();
+
+(window as any)['apolloCache'] = apolloCache;
+
+const stateLink = withClientState({
+    cache: apolloCache,
+    resolvers: {
+        Mutation: {
+            requestStarted: (_, __, { cache }) => {
+                const query = gql`
+                    query GetInFlightRequests {
+                        network @client {
+                            inFlightRequests
+                        }
+                    }
+                `;
+                const previous = cache.readQuery({ query });
+                const data = {
+                    network: {
+                        __typename: 'Network',
+                        inFlightRequests: previous.network.inFlightRequests + 1,
+                    },
+                };
+                cache.writeData({ data });
+                return null;
+            },
+            requestCompleted: (_, __, { cache }) => {
+                const query = gql`
+                    query GetInFlightRequests {
+                        network @client {
+                            inFlightRequests
+                        }
+                    }
+                `;
+                const previous = cache.readQuery({ query });
+                const data = {
+                    network: {
+                        __typename: 'Network',
+                        inFlightRequests: previous.network.inFlightRequests - 1,
+                    },
+                };
+                cache.writeData({ data });
+                return null;
+            },
+        },
+    },
+    defaults: {
+        network: {
+            inFlightRequests: 0,
+            __typename: 'Network',
+        },
+    },
+});
+
+export function createApollo(httpLink: HttpLink) {
+    return {
+        link:  ApolloLink.from([stateLink, httpLink.create({ uri: `${API_URL}/${API_PATH}` })]),
+        cache: apolloCache,
+    };
+}
+
+@NgModule({
+    imports: [
+        ApolloModule,
+        HttpLinkModule,
+        HttpClientModule,
+    ],
+    exports: [],
+    declarations: [],
+    providers: [
+        BaseDataService,
+        DataService,
+        {
+            provide: APOLLO_OPTIONS,
+            useFactory: createApollo,
+            deps: [HttpLink],
+        },
+        { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
+    ],
+})
+export class DataModule {
+}

+ 7 - 2
admin-ui/src/app/core/providers/data/base-data.service.ts → admin-ui/src/app/data/providers/base-data.service.ts

@@ -5,8 +5,9 @@ import { DocumentNode } from 'graphql/language/ast';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
-import { API_URL } from '../../../app.config';
-import { LocalStorageService } from '../local-storage/local-storage.service';
+import { API_URL } from '../../app.config';
+import { LocalStorageService } from '../../core/providers/local-storage/local-storage.service';
+import { FetchResult } from 'apollo-link';
 
 @Injectable()
 export class BaseDataService {
@@ -30,6 +31,10 @@ export class BaseDataService {
         });
     }
 
+    mutate(mutation: DocumentNode): Observable<FetchResult> {
+        return this.apollo.mutate({ mutation });
+    }
+
     /**
      * Perform REST-like POST
      */

+ 3 - 0
admin-ui/src/app/core/providers/data/data.service.ts → admin-ui/src/app/data/providers/data.service.ts

@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
 
 import { BaseDataService } from './base-data.service';
+import { LocalDataService } from './local-data.service';
 import { ProductDataService } from './product-data.service';
 import { UserDataService } from './user-data.service';
 
@@ -8,10 +9,12 @@ import { UserDataService } from './user-data.service';
 export class DataService {
     user: UserDataService;
     product: ProductDataService;
+    local: LocalDataService;
 
     constructor(baseDataService: BaseDataService) {
         this.user = new UserDataService(baseDataService);
         this.product = new ProductDataService(baseDataService);
+        this.local = new LocalDataService(baseDataService);
     }
 
 }

+ 7 - 2
admin-ui/src/app/core/providers/data/interceptor.ts → admin-ui/src/app/data/providers/interceptor.ts

@@ -4,8 +4,9 @@ import { Injectable } from '@angular/core';
 import { Observable, throwError } from 'rxjs';
 import { catchError, tap } from 'rxjs/operators';
 
-import { ApiActions } from '../../../state/api/api-actions';
-import { NotificationService } from '../notification/notification.service';
+import { ApiActions } from '../../state/api/api-actions';
+import { NotificationService } from '../../core/providers/notification/notification.service';
+import { DataService } from './data.service';
 
 /**
  * The default interceptor examines all HTTP requests & responses and automatically updates the requesting state
@@ -15,22 +16,26 @@ import { NotificationService } from '../notification/notification.service';
 export class DefaultInterceptor implements HttpInterceptor {
 
     constructor(private apiActions: ApiActions,
+                private dataService: DataService,
                 private notification: NotificationService) {}
 
     intercept(req: HttpRequest<any>, next: HttpHandler):
         Observable<HttpEvent<any>> {
         this.apiActions.startRequest();
+        this.dataService.local.startRequest().subscribe();
         return next.handle(req).pipe(
             tap(event => {
                     if (event instanceof HttpResponse) {
                         this.notifyOnGraphQLErrors(event);
                         this.apiActions.requestCompleted();
+                        this.dataService.local.completeRequest().subscribe();
                     }
                 },
                 err => {
                     if (err instanceof HttpErrorResponse) {
                         this.notification.error(err.message);
                         this.apiActions.requestCompleted();
+                        this.dataService.local.completeRequest().subscribe();
                     }
                 }),
         );

+ 40 - 0
admin-ui/src/app/data/providers/local-data.service.ts

@@ -0,0 +1,40 @@
+import { Observable } from 'rxjs';
+
+import gql from 'graphql-tag';
+import { map } from 'rxjs/operators';
+import { BaseDataService } from './base-data.service';
+
+export class LocalDataService {
+
+    constructor(private baseDataService: BaseDataService) {}
+
+    startRequest() {
+        return this.baseDataService.mutate(gql`
+            mutation {
+                requestStarted @client
+            }
+        `);
+    }
+
+    completeRequest() {
+        return this.baseDataService.mutate(gql`
+            mutation {
+                requestCompleted @client
+            }
+        `);
+    }
+
+    inFlightRequests(): Observable<number> {
+        return this.baseDataService.query<any>(gql`
+            query {
+                network @client {
+                    inFlightRequests
+                }
+            }
+        `).valueChanges.pipe(
+            map(result => result.data.network.inFlightRequests),
+        );
+    }
+
+}
+

+ 4 - 4
admin-ui/src/app/core/providers/data/product-data.service.ts → admin-ui/src/app/data/providers/product-data.service.ts

@@ -1,15 +1,15 @@
 import { QueryRef } from 'apollo-angular';
 
-import { ID } from '../../../../../../shared/shared-types';
-import { getProductById } from '../../../common/queries/get-product-by-id';
-import { getProductList } from '../../../common/queries/get-product-list';
+import { ID } from '../../../../../shared/shared-types';
+import { getProductById } from '../../common/queries/get-product-by-id';
+import { getProductList } from '../../common/queries/get-product-list';
 import {
     GetProductByIdQuery,
     GetProductByIdQueryVariables,
     GetProductListQuery,
     GetProductListQueryVariables,
     LanguageCode,
-} from '../../../common/types/gql-generated-types';
+} from '../../common/types/gql-generated-types';
 
 import { BaseDataService } from './base-data.service';
 

+ 1 - 1
admin-ui/src/app/core/providers/data/user-data.service.ts → admin-ui/src/app/data/providers/user-data.service.ts

@@ -1,6 +1,6 @@
 import { Observable } from 'rxjs';
 
-import { LoginResponse, UserResponse } from '../../../common/types/response';
+import { LoginResponse, UserResponse } from '../../common/types/response';
 
 import { BaseDataService } from './base-data.service';
 

+ 1 - 0
admin-ui/src/app/login/components/login/login.component.ts

@@ -36,6 +36,7 @@ export class LoginComponent {
                         default:
                             this.lastError = err.message;
                     }
+                    console.log(err);
                 });
     }
 

+ 1 - 1
admin-ui/src/app/state/user/user-actions.ts

@@ -2,7 +2,7 @@ import {Injectable} from '@angular/core';
 import { EMPTY, Observable, of, throwError } from 'rxjs';
 import { catchError, map } from 'rxjs/operators';
 
-import { DataService } from '../../core/providers/data/data.service';
+import { DataService } from '../../data/providers/data.service';
 import { LocalStorageService } from '../../core/providers/local-storage/local-storage.service';
 import { handleError } from '../handle-error';
 import { StateStore } from '../state-store.service';

+ 19 - 0
admin-ui/yarn.lock

@@ -659,6 +659,13 @@ apollo-link-dedup@^1.0.0:
   dependencies:
     apollo-link "^1.2.2"
 
+apollo-link-state@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.1.tgz#65e9e0e12c67936b8c4b12b8438434f393104579"
+  dependencies:
+    apollo-utilities "^1.0.8"
+    graphql-anywhere "^4.1.0-alpha.0"
+
 apollo-link@^1.0.0, apollo-link@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.2.tgz#54c84199b18ac1af8d63553a68ca389c05217a03"
@@ -673,6 +680,12 @@ apollo-utilities@^1.0.0, apollo-utilities@^1.0.15:
   dependencies:
     fast-json-stable-stringify "^2.0.0"
 
+apollo-utilities@^1.0.16, apollo-utilities@^1.0.8:
+  version "1.0.16"
+  resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.16.tgz#787310df4c3900a68c0beb3d351c59725a588cdb"
+  dependencies:
+    fast-json-stable-stringify "^2.0.0"
+
 app-root-path@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
@@ -2833,6 +2846,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
+graphql-anywhere@^4.1.0-alpha.0:
+  version "4.1.14"
+  resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.14.tgz#89664cb885faaec1cbc66905351fadae8cc85a04"
+  dependencies:
+    apollo-utilities "^1.0.16"
+
 graphql-anywhere@^4.1.13:
   version "4.1.13"
   resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.13.tgz#5314b879b0b7066e6835ad2f5716b4dae381dc63"