diff --git a/.deploy/api/README.md b/.deploy/api/README.md index c45406b7e..6345bc269 100644 --- a/.deploy/api/README.md +++ b/.deploy/api/README.md @@ -68,7 +68,7 @@ oc delete all,configmap,secret -l app=ngx-starter-kit-api -n ngx-starter-kit # redeploy # From OpenShift Console UI -Applications > Deployments > ngx-starter-kit > Deploy +Applications > Deployments > ngx-starter-kit > Deploy ``` #### Kubernetes Deployment diff --git a/PLAYBOOK.md b/PLAYBOOK.md index 8c28a7267..57eb9c009 100644 --- a/PLAYBOOK.md +++ b/PLAYBOOK.md @@ -10,10 +10,11 @@ Do-it-yourself step-by-step instructions to create this project structure from s | -------------------- | ------- | -------- | | Node | v11.1.0 | | | NPM | v6.4.1 | | -| Angular CLI | v7.1.0 | | -| @nrwl/schematics | v7.1.0 | | +| Angular CLI | v7.1.2 | | +| @nrwl/schematics | v7.1.1 | | | @nestjs/cli | v5.6.3 | | -| semantic-release-cli | v4.0.11 | | +| semantic-release-cli | v4.0.12 | | +| commitizen | v3.0.5 | | ### Install Prerequisites @@ -61,7 +62,7 @@ npm remove -g @nestjs/cli npm remove -g semantic-release-cli npm remove -g commitizen -npm install -g @angular/cli@next +npm install -g @angular/cli npm install -g @nrwl/schematics npm install -g @nestjs/cli npm install -g semantic-release-cli @@ -76,6 +77,8 @@ ng config -g schematics.@nrwl/schematics:component.styleext scss ng config -g cli.packageManager npm # set jest as default TestRunner ng config -g schematics.@nrwl/schematics:library.unitTestRunner jest +# set scss as default styleext for ngx-formly +ng config -g schematics@ngx-formly/schematics:component.styleext scss # check your global defaults more cat ~/.angular-config.json # show dependency tree for specified package. @@ -235,6 +238,7 @@ ng update @nrwl/schematics --force # generate `Lazy-loaded Feature Modules` ng g lib home --routing --lazy --prefix=ngx --parent-module=apps/webapp/src/app/app.module.ts --unit-test-runner=jest --tags=layout,entry-module ng g lib dashboard --routing --lazy --prefix=ngx --parent-module=apps/webapp/src/app/app.module.ts --unit-test-runner=jest --tags=layout,entry-module +ng g lib admin --routing --lazy --prefix=ngx --parent-module=apps/webapp/src/app/app.module.ts --unit-test-runner=jest --tags=layout,entry-module ng g lib NotFound --routing --lazy --prefix=ngx --parent-module=apps/webapp/src/app/app.module.ts --unit-test-runner=jest --tags=entry-module ng g lib experiments --routing --lazy --prefix=ngx --parent-module=libs/dashboard/src/lib/dashboard.module.ts --unit-test-runner=jest --tags=child-module ng g lib widgets --routing --lazy --prefix=ngx --parent-module=libs/dashboard/src/lib/dashboard.module.ts --unit-test-runner=jest --tags=child-module @@ -386,75 +390,91 @@ ng g service directives/in-viewport/Viewport --project=ngx-utils --module=in-vie # generate components for `toolbar` Module ng g lib toolbar --prefix=ngx --tags=private-module --unit-test-runner=jest -d -ng g component toolbar --project=toolbar --flat -d -ng g component components/search --project=toolbar -d -ng g component components/searchBar --project=toolbar -ng g component components/UserMenu --project=toolbar -ng g component components/FullscreenToggle --project=toolbar -d +ng g component toolbar --project=toolbar --flat -d +ng g component components/search --project=toolbar -d +ng g component components/searchBar --project=toolbar -d +ng g component components/UserMenu --project=toolbar -d +ng g component components/FullscreenToggle --project=toolbar -d ng g component components/SidenavMobileToggle --project=toolbar -d -ng g component components/QuickpanelToggle --project=toolbar -d +ng g component components/QuickpanelToggle --project=toolbar -d # generate components for `sidenav` Module ng g lib sidenav --prefix=ngx --tags=private-module --unit-test-runner=jest -d -ng g component sidenav --project=sidenav --flat -d -ng g component components/sidenavItem --project=sidenav -d -ng g directive IconSidenav --project=sidenav -d +ng g component sidenav --project=sidenav --flat -d +ng g component components/sidenavItem --project=sidenav -d +ng g directive IconSidenav --project=sidenav -d # generate components for `auth` Module ng g lib auth --prefix=ngx --tags=private-module,core-module --prefix=ngx --style=scss --unit-test-runner=jest -d ng g component components/login --project=auth -d +ng g guard admin --project=auth -d ng g @ngxs/schematics:store --name=auth --spec --project=auth -d # generate components for `navigator` Module ng g lib navigator --prefix=ngx --tags=private-module,core-module --unit-test-runner=jest -d -ng g service services/menu --project=navigator -d -ng g class models/menuItem --project=navigator --type=model -d -ng g class state/menu --project=navigator --type=state -d +ng g service services/menu --project=navigator -d +ng g class models/menuItem --project=navigator --type=model -d +ng g class state/menu --project=navigator --type=state -d # generate containers, components for `home` Module -ng g component components/header --project=home -ng g component containers/homeLayout --project=home -ng g component containers/landing --project=home -ng g component containers/blog --project=home -ng g component containers/about --project=home +ng g component components/header --project=home +ng g component containers/homeLayout --project=home +ng g component containers/landing --project=home +ng g component containers/blog --project=home +ng g component containers/about --project=home # generate containers, components for `dashboard` Module -ng g component components/rainbow --project=dashboard -d +ng g component components/rainbow --project=dashboard -d ng g component containers/dashboardLayout --project=dashboard -d -ng g component containers/overview --project=dashboard -d -ng g component containers/profile --project=dashboard -d -ng g component containers/settings --project=dashboard -d +ng g component containers/overview --project=dashboard -d +ng g component containers/profile --project=dashboard -d +ng g component containers/settings --project=dashboard -d # generate containers, components for `widgets` Module ng g component containers/wizdash --project=widgets -d # generate containers, components for `grid` Module -ng g component containers/AccountsTable --project=grid -d -ng g component components/AccountDetail --project=grid -d -ng g component components/AccountEdit --project=grid -d -ng g class models/account --type=model --project=grid -d -ng g service services/account --project=grid -d +ng g component containers/AccountsTable --project=grid -d +ng g component components/AccountDetail --project=grid -d +ng g component components/AccountEdit --project=grid -d +ng g class models/account --project=grid --type=model -d +ng g service services/account --project=grid -d ng g component containers/AccountsGridList --project=grid -d # generate containers, components for `experiments` Module -ng g component containers/animations --project=experiments -d -ng g component components/hammerCard --project=experiments -d +ng g component containers/animations --project=experiments -d +ng g component components/hammerCard --project=experiments -d ng g directive components/Hammertime/Hammertime --project=experiments -d -ng g component containers/ContextMenu --project=experiments -d -ng g component containers/FileUpload --project=experiments -d -ng g component containers/virtualScroll --project=experiments -d -ng g component containers/StickyTable --project=experiments -d -ng g component containers/clapButton --project=experiments -s -t --spec=false -d -ng g component containers/knobDemo --project=experiments -d -ng g component containers/ledDemo --project=experiments -d -ng g component containers/ImageComp --project=experiments -d -ng g component containers/layout --project=experiments -d -ng g component components/card --project=experiments -d -ng g component containers/viewport --project=experiments --spec=false -d +ng g component containers/ContextMenu --project=experiments -d +ng g component containers/FileUpload --project=experiments -d +ng g component containers/virtualScroll --project=experiments -d +ng g component containers/StickyTable --project=experiments -d +ng g component containers/clapButton --project=experiments -s -t --spec=false -d +ng g component containers/knobDemo --project=experiments -d +ng g component containers/ledDemo --project=experiments -d +ng g component containers/ImageComp --project=experiments -d +ng g component containers/layout --project=experiments -d +ng g component components/card --project=experiments -d +ng g component containers/viewport --project=experiments --spec=false -d # generate components for `ImageComparison` Module ng g lib ImageComparison --prefix=ngx --tags=public-module --spec=false --publishable=true -d ng g component ImageComparison --project=image-comparison --export --flat -d + + +# generate containers, components for `admin` Module +ng g component containers/overview --project=admin -d +ng g component containers/adminLayout --project=admin -d + +ng g component containers/notifications --project=admin -d +ng g component components/notificationDetail --project=admin -d +ng g component components/notificationEdit --project=admin -d +ng g service services/notification --project=admin -d + +ng g component containers/subscriptions --project=admin -d +ng g component components/subscriptionDetail --project=admin -d +ng g class models/subscription --project=admin --type=model -d +ng g service services/subscription --project=admin -d ``` #### Workspace Schematics diff --git a/angular.json b/angular.json index 0bd66e1e3..dfdfa74bc 100644 --- a/angular.json +++ b/angular.json @@ -1471,6 +1471,34 @@ } } } + }, + "admin": { + "root": "libs/admin", + "sourceRoot": "libs/admin/src", + "projectType": "library", + "prefix": "ngx", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "libs/admin/tsconfig.lib.json", + "libs/admin/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + }, + "test": { + "builder": "@nrwl/builders:jest", + "options": { + "jestConfig": "libs/admin/jest.config.js", + "tsConfig": "libs/admin/tsconfig.spec.json", + "setupFile": "libs/admin/src/test-setup.ts" + } + } + } } }, "schematics": { @@ -1483,6 +1511,9 @@ }, "@nrwl/schematics:library": { "unitTestRunner": "jest" + }, + "@ngx-formly/schematics:component": { + "styleext": "scss" } }, "cli": { diff --git a/apps/api/src/app/notifications/notification/dto/update-notification.dto.ts b/apps/api/src/app/notifications/notification/dto/update-notification.dto.ts new file mode 100644 index 000000000..3b3ec70ad --- /dev/null +++ b/apps/api/src/app/notifications/notification/dto/update-notification.dto.ts @@ -0,0 +1,48 @@ +import { ApiModelProperty, ApiModelPropertyOptional } from '@nestjs/swagger'; +import { IsAscii, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; +import { NotificationColor, NotificationIcon, TargetType } from '../notification.entity'; +import { Column, Index } from 'typeorm'; + +export class UpdateNotificationDto { + @ApiModelProperty({ type: String, minLength: 10, maxLength: 100 }) + @IsNotEmpty() + @IsString() + readonly title: string; + + @ApiModelProperty({ type: String, minLength: 10, maxLength: 100 }) + @IsNotEmpty() + @IsString() + readonly body: string; + + @ApiModelProperty({ type: String, minLength: 3, maxLength: 50 }) + @IsNotEmpty() + @IsAscii() + @MinLength(3) + @MaxLength(50) + readonly target: string; + + @ApiModelProperty({ type: String, enum: TargetType }) + @IsNotEmpty() + @IsEnum(TargetType) + readonly targetType: TargetType; + + @ApiModelPropertyOptional({ type: String, enum: NotificationIcon, default: NotificationIcon.NOTIFICATIONS }) + @IsOptional() + @IsEnum(NotificationIcon) + readonly icon?: NotificationIcon; + + @ApiModelPropertyOptional({ type: String, enum: NotificationColor, default: NotificationColor.PRIMARY }) + @IsOptional() + @IsEnum(NotificationColor) + readonly color?: NotificationColor; + + @ApiModelPropertyOptional({ type: Boolean, default: false }) + @IsOptional() + @IsBoolean() + readonly native?: boolean; + + @ApiModelPropertyOptional({ type: Boolean, default: false }) + @IsOptional() + @Index() + readonly read?: boolean; +} diff --git a/libs/admin/jest.config.js b/libs/admin/jest.config.js new file mode 100644 index 000000000..df81f8c44 --- /dev/null +++ b/libs/admin/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + name: 'admin', + preset: '../../jest.config.js', + coverageDirectory: '../../coverage/libs/admin', +}; diff --git a/libs/admin/src/index.ts b/libs/admin/src/index.ts new file mode 100644 index 000000000..39f439bae --- /dev/null +++ b/libs/admin/src/index.ts @@ -0,0 +1 @@ +export * from './lib/admin.module'; diff --git a/libs/admin/src/lib/admin.module.spec.ts b/libs/admin/src/lib/admin.module.spec.ts new file mode 100644 index 000000000..7d56887b1 --- /dev/null +++ b/libs/admin/src/lib/admin.module.spec.ts @@ -0,0 +1,14 @@ +import { async, TestBed } from '@angular/core/testing'; +import { AdminModule } from './admin.module'; + +describe('AdminModule', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [AdminModule], + }).compileComponents(); + })); + + it('should create', () => { + expect(AdminModule).toBeDefined(); + }); +}); diff --git a/libs/admin/src/lib/admin.module.ts b/libs/admin/src/lib/admin.module.ts new file mode 100644 index 000000000..6b639c9f6 --- /dev/null +++ b/libs/admin/src/lib/admin.module.ts @@ -0,0 +1,79 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '@ngx-starter-kit/shared'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { AppConfirmModule } from '@ngx-starter-kit/app-confirm'; +import { HelperModule, TruncateModule } from '@ngx-starter-kit/ngx-utils'; +import { OverviewComponent } from './containers/overview/overview.component'; +import { SubscriptionsComponent } from './containers/subscriptions/subscriptions.component'; +import { SubscriptionDetailComponent } from './components/subscription-detail/subscription-detail.component'; +import { NotificationsComponent } from './containers/notifications/notifications.component'; +import { NotificationDetailComponent } from './components/notification-detail/notification-detail.component'; +import { NotificationEditComponent } from './components/notification-edit/notification-edit.component'; +import { AdminLayoutComponent } from './containers/admin-layout/admin-layout.component'; +import { ToolbarModule } from '@ngx-starter-kit/toolbar'; +import { QuickpanelModule } from '@ngx-starter-kit/quickpanel'; +import { AdminGuard } from '@ngx-starter-kit/auth'; +import { FormlyModule } from '@ngx-formly/core'; +import { FormlyMaterialModule } from '@ngx-formly/material'; + +@NgModule({ + imports: [ + SharedModule, + DragDropModule, + AppConfirmModule, + TruncateModule, + HelperModule, + ToolbarModule, + QuickpanelModule, + FormlyModule.forRoot(), + FormlyMaterialModule, + RouterModule.forChild([ + /* {path: '', pathMatch: 'full', component: InsertYourComponentHere} */ + { + path: '', + component: AdminLayoutComponent, + canActivate: [AdminGuard], + data: { title: 'Admin', depth: 1 }, + children: [ + { path: '', component: OverviewComponent, data: { title: 'Overview', depth: 2 } }, + { + path: 'subscriptions', + component: SubscriptionsComponent, + data: { title: 'Subscriptions', depth: 3 }, + children: [ + { + path: ':id', + component: SubscriptionDetailComponent, + data: { title: 'Subscription Detail' }, + }, + ], + }, + { + path: 'notifications', + component: NotificationsComponent, + data: { title: 'Notifications', depth: 3 }, + children: [ + { + path: ':id', + component: NotificationDetailComponent, + data: { title: 'Notification Detail' }, + }, + ], + }, + ], + }, + ]), + ], + declarations: [ + OverviewComponent, + NotificationsComponent, + SubscriptionsComponent, + SubscriptionDetailComponent, + NotificationDetailComponent, + NotificationEditComponent, + AdminLayoutComponent, + ], + entryComponents: [NotificationEditComponent], +}) +export class AdminModule {} diff --git a/libs/admin/src/lib/components/notification-detail/notification-detail.component.html b/libs/admin/src/lib/components/notification-detail/notification-detail.component.html new file mode 100644 index 000000000..89a5c9b49 --- /dev/null +++ b/libs/admin/src/lib/components/notification-detail/notification-detail.component.html @@ -0,0 +1,15 @@ + + +

Selected Notification

+
+ + + + +
+ {{ entry.key }}:{{ entry.value | json }} +
+
+
+
+
diff --git a/libs/admin/src/lib/components/notification-detail/notification-detail.component.scss b/libs/admin/src/lib/components/notification-detail/notification-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/components/notification-detail/notification-detail.component.spec.ts b/libs/admin/src/lib/components/notification-detail/notification-detail.component.spec.ts new file mode 100644 index 000000000..7cab71d38 --- /dev/null +++ b/libs/admin/src/lib/components/notification-detail/notification-detail.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationDetailComponent } from './notification-detail.component'; + +describe('NotificationDetailComponent', () => { + let component: NotificationDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [NotificationDetailComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/components/notification-detail/notification-detail.component.ts b/libs/admin/src/lib/components/notification-detail/notification-detail.component.ts new file mode 100644 index 000000000..c2999177f --- /dev/null +++ b/libs/admin/src/lib/components/notification-detail/notification-detail.component.ts @@ -0,0 +1,34 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Subscription as RxSubscription } from 'rxjs'; +import { fadeAnimation } from '@ngx-starter-kit/animations'; + +import { NotificationService } from '../../services/notification.service'; +import { AppNotification } from '@ngx-starter-kit/notifications'; + +@Component({ + selector: 'ngx-notification-detail', + templateUrl: './notification-detail.component.html', + styleUrls: ['./notification-detail.component.scss'], + animations: [fadeAnimation], +}) +export class NotificationDetailComponent implements OnInit, OnDestroy { + notification: AppNotification; + sub: RxSubscription; + animationTrigger$ = new BehaviorSubject(''); + + constructor(private notificationService: NotificationService, private route: ActivatedRoute) {} + + ngOnInit() { + this.sub = this.route.params.subscribe(params => { + this.notificationService.getById(params['id']).subscribe(data => { + this.animationTrigger$.next(params['id']); + this.notification = data; + }); + }); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } +} diff --git a/libs/admin/src/lib/components/notification-edit/notification-edit.component.html b/libs/admin/src/lib/components/notification-edit/notification-edit.component.html new file mode 100644 index 000000000..cd5b4c129 --- /dev/null +++ b/libs/admin/src/lib/components/notification-edit/notification-edit.component.html @@ -0,0 +1,13 @@ +

{{ title }}

+ +
+ + + + + + + +
diff --git a/libs/admin/src/lib/components/notification-edit/notification-edit.component.scss b/libs/admin/src/lib/components/notification-edit/notification-edit.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/components/notification-edit/notification-edit.component.spec.ts b/libs/admin/src/lib/components/notification-edit/notification-edit.component.spec.ts new file mode 100644 index 000000000..09672faf8 --- /dev/null +++ b/libs/admin/src/lib/components/notification-edit/notification-edit.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationEditComponent } from './notification-edit.component'; + +describe('NotificationEditComponent', () => { + let component: NotificationEditComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [NotificationEditComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/components/notification-edit/notification-edit.component.ts b/libs/admin/src/lib/components/notification-edit/notification-edit.component.ts new file mode 100644 index 000000000..ba7d2f8a0 --- /dev/null +++ b/libs/admin/src/lib/components/notification-edit/notification-edit.component.ts @@ -0,0 +1,102 @@ +import { Component, Inject } from '@angular/core'; +import { EntityFormComponent } from '@ngx-starter-kit/shared'; +import { AppNotification, NotificationColor, NotificationIcon, TargetType } from '@ngx-starter-kit/notifications'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; + +function enumToOptions(enumType): { label: string; value: any }[] { + return Object.keys(enumType).map(key => ({ label: key, value: enumType[key] })); +} + +@Component({ + selector: 'ngx-notification-edit', + templateUrl: './notification-edit.component.html', + styleUrls: ['./notification-edit.component.scss'], +}) +export class NotificationEditComponent extends EntityFormComponent { + model; + fields: FormlyFieldConfig[]; + options: FormlyFormOptions = {}; + constructor( + @Inject(MAT_DIALOG_DATA) public data: { title: string; payload: AppNotification }, + public dialogRef: MatDialogRef, + private fb: FormBuilder, + ) { + super(data, dialogRef); + } + + /* Optional */ + // tslint:disable-next-line + ngOnInit() { + super.ngOnInit(); + } + + buildForm(item: AppNotification) { + this.entityForm = new FormGroup({}); + this.model = item; + this.fields = [ + { + key: 'title', + type: 'input', + templateOptions: { + type: 'email', + label: 'Title', + placeholder: 'title', + required: true, + }, + }, + { + key: 'body', + type: 'input', + templateOptions: { + label: 'Body', + required: true, + }, + }, + { + key: 'target', + type: 'input', + templateOptions: { + label: 'Target', + required: true, + }, + }, + { + key: 'targetType', + type: 'select', + templateOptions: { + label: 'TargetType', + options: enumToOptions(TargetType), + required: true, + }, + }, + { + key: 'icon', + type: 'select', + templateOptions: { + label: 'Icon', + options: enumToOptions(NotificationIcon), + required: true, + }, + }, + { + key: 'color', + type: 'select', + templateOptions: { + label: 'Color', + options: enumToOptions(NotificationColor), + required: true, + }, + }, + { + key: 'native', + type: 'checkbox', + templateOptions: { + label: 'Native', + required: true, + }, + }, + ]; + } +} diff --git a/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.html b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.html new file mode 100644 index 000000000..1f8964ecb --- /dev/null +++ b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.html @@ -0,0 +1,15 @@ + + +

Selected Subscription

+
+ + + + +
+ {{ entry.key }}:{{ entry.value | json }} +
+
+
+
+
diff --git a/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.scss b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.spec.ts b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.spec.ts new file mode 100644 index 000000000..8ac54f2f3 --- /dev/null +++ b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubscriptionDetailComponent } from './subscription-detail.component'; + +describe('SubscriptionDetailComponent', () => { + let component: SubscriptionDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SubscriptionDetailComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubscriptionDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.ts b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.ts new file mode 100644 index 000000000..a640a310d --- /dev/null +++ b/libs/admin/src/lib/components/subscription-detail/subscription-detail.component.ts @@ -0,0 +1,34 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Subscription as RxSubscription } from 'rxjs'; +import { fadeAnimation } from '@ngx-starter-kit/animations'; + +import { SubscriptionService } from '../../services/subscription.service'; +import { Subscription } from '../../models/subscription.model'; + +@Component({ + selector: 'ngx-subscription-detail', + templateUrl: './subscription-detail.component.html', + styleUrls: ['./subscription-detail.component.scss'], + animations: [fadeAnimation], +}) +export class SubscriptionDetailComponent implements OnInit, OnDestroy { + subscription: Subscription; + sub: RxSubscription; + animationTrigger$ = new BehaviorSubject(''); + + constructor(private subscriptionService: SubscriptionService, private route: ActivatedRoute) {} + + ngOnInit() { + this.sub = this.route.params.subscribe(params => { + this.subscriptionService.getById(params['id']).subscribe(data => { + this.animationTrigger$.next(params['id']); + this.subscription = data; + }); + }); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } +} diff --git a/libs/admin/src/lib/containers/admin-layout/admin-layout.component.html b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.html new file mode 100644 index 000000000..5538fb3d3 --- /dev/null +++ b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ + +
+
+
+
+
diff --git a/libs/admin/src/lib/containers/admin-layout/admin-layout.component.scss b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.scss new file mode 100644 index 000000000..8c4a65496 --- /dev/null +++ b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.scss @@ -0,0 +1,5 @@ +.wrapper { + display: block; + padding: 1.5%; + position: relative; +} diff --git a/libs/admin/src/lib/containers/admin-layout/admin-layout.component.spec.ts b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.spec.ts new file mode 100644 index 000000000..0bd07083c --- /dev/null +++ b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminLayoutComponent } from './admin-layout.component'; + +describe('AdminLayoutComponent', () => { + let component: AdminLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [AdminLayoutComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/containers/admin-layout/admin-layout.component.ts b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.ts new file mode 100644 index 000000000..d1e3fd347 --- /dev/null +++ b/libs/admin/src/lib/containers/admin-layout/admin-layout.component.ts @@ -0,0 +1,39 @@ +import { AfterViewInit, Component, ComponentRef, Inject, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { hierarchicalRouteAnimation } from '@ngx-starter-kit/animations'; +import { RouterState } from '@ngxs/router-plugin'; +import { map } from 'rxjs/operators'; +import { Store } from '@ngxs/store'; +import { WINDOW } from '@ngx-starter-kit/core'; +import { NotificationsComponent } from '../notifications/notifications.component'; + +@Component({ + selector: 'ngx-admin-layout', + templateUrl: './admin-layout.component.html', + styleUrls: ['./admin-layout.component.scss'], + animations: [hierarchicalRouteAnimation], +}) +export class AdminLayoutComponent implements OnInit { + // @ViewChild('sumo') sumoTpl: TemplateRef; + @ViewChild('demo') demoTpl: TemplateRef; + quickpanelOpen = false; + crumbs$; + depth$; + + constructor(private store: Store, @Inject(WINDOW) private window: Window) {} + + ngOnInit() { + this.crumbs$ = this.store + .select(RouterState.state) + .pipe(map(state => Array.from(state.breadcrumbs, ([key, value]) => ({ name: key, link: '/' + value })))); + + this.depth$ = this.store.select(RouterState.state).pipe(map(state => state.data.depth)); + } + + onActivate(componentRef: ComponentRef) { + // HINT: I can set inputs on activated component! + // if (componentRef instanceof NotificationsComponent) { + // componentRef.sayhello(); + // componentRef.crumbs$ = this.crumbs$; + // } + } +} diff --git a/libs/admin/src/lib/containers/notifications/cell.templates.html b/libs/admin/src/lib/containers/notifications/cell.templates.html new file mode 100644 index 000000000..5c8a5e902 --- /dev/null +++ b/libs/admin/src/lib/containers/notifications/cell.templates.html @@ -0,0 +1,12 @@ + + + + + + + + + +
{{ row.id }} -- {{ column }}
diff --git a/libs/admin/src/lib/containers/notifications/notifications.component.html b/libs/admin/src/lib/containers/notifications/notifications.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/containers/notifications/notifications.component.scss b/libs/admin/src/lib/containers/notifications/notifications.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/containers/notifications/notifications.component.spec.ts b/libs/admin/src/lib/containers/notifications/notifications.component.spec.ts new file mode 100644 index 000000000..5970f6de6 --- /dev/null +++ b/libs/admin/src/lib/containers/notifications/notifications.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsComponent } from './notifications.component'; + +describe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [NotificationsComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/containers/notifications/notifications.component.ts b/libs/admin/src/lib/containers/notifications/notifications.component.ts new file mode 100644 index 000000000..5e663da15 --- /dev/null +++ b/libs/admin/src/lib/containers/notifications/notifications.component.ts @@ -0,0 +1,162 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { Navigate } from '@ngxs/router-plugin'; +import { MatDialog, MatSnackBar } from '@angular/material'; +import { throwError } from 'rxjs'; +import { catchError, concatMap, filter, map, mergeMap, tap } from 'rxjs/operators'; +import { formatDistance } from 'date-fns/esm'; +import { AppConfirmService } from '@ngx-starter-kit/app-confirm'; +import { EntitiesComponent, EntityColumnDef } from '@ngx-starter-kit/shared'; +import { AppNotification, TargetType, NotificationColor, NotificationIcon } from '@ngx-starter-kit/notifications'; +import { NotificationService } from '../../services/notification.service'; +import { NotificationEditComponent } from '../../components/notification-edit/notification-edit.component'; + +const entityHtmlTpl = require('../../../../../shared/src/lib/containers/entity/entity.component.html'); +const cellHtmlTpl = require('./cell.templates.html'); + +// tslint:disable-next-line +@Component({ + selector: 'ngx-notifications', + template: entityHtmlTpl + cellHtmlTpl, + // templateUrl: '../../../../../shared/src/lib/containers/entity/entity.component.html', + styleUrls: [ + './notifications.component.scss', + '../../../../../shared/src/lib/containers/entity/entity.component.scss', + ], +}) +export class NotificationsComponent extends EntitiesComponent { + @ViewChild('send') sendTpl: TemplateRef; + // @ViewChild('editDelete') editDeleteTpl: TemplateRef; + columns: EntityColumnDef[]; + + // optional + readonly showActionColumn = true; + readonly showColumnFilter = true; + readonly showToolbar = true; + readonly formRef = NotificationEditComponent; + + constructor( + private notificationService: NotificationService, + private store: Store, + private dialog: MatDialog, + private snack: MatSnackBar, + private confirmService: AppConfirmService, + ) { + super(notificationService); + } + + // tslint:disable-next-line + ngOnInit() { + super.ngOnInit(); + this.columns = [ + new EntityColumnDef({ property: 'id', header: 'No.', sticky: 'start' }), + new EntityColumnDef({ property: 'title', header: 'Title' }), + new EntityColumnDef({ property: 'body', header: 'Body' }), + new EntityColumnDef({ property: 'target', header: 'Target' }), + new EntityColumnDef({ property: 'targetType', header: 'Type' }), + new EntityColumnDef({ property: 'icon', header: 'Icon' }), + new EntityColumnDef({ property: 'color', header: 'Color' }), + new EntityColumnDef({ property: 'read', header: 'Read' }), + new EntityColumnDef({ property: 'native', header: 'Native' }), + new EntityColumnDef({ property: 'isActive', header: 'Active' }), + new EntityColumnDef({ + property: 'createdAt', + header: 'Created', + displayFn: entity => `${formatDistance(entity.createdAt, new Date(), { addSuffix: true })}`, + }), + new EntityColumnDef({ + property: 'updatedAt', + header: 'Updated', + displayFn: entity => `${formatDistance(entity.updatedAt, new Date(), { addSuffix: true })}`, + }), + new EntityColumnDef({ property: 'send', header: 'Send', template: this.sendTpl }), + // new EntityColumnDef({ property: 'actions', header: 'Actions', template: this.editDeleteTpl }), + ] as EntityColumnDef[]; + } + + // optional + delete(item: AppNotification) { + return this.confirmService.confirm('Confirm', `Delete Notification(${item.id})?`).pipe( + filter(confirmed => confirmed === true), + mergeMap(_ => super.delete(item)), + tap(_ => { + this.snack.open('Notification Deleted!', 'OK', { duration: 5000 }); + this.store.dispatch(new Navigate([`/admin/notifications`])); + }), + catchError(error => { + this.snack.open(error, 'OK', { duration: 10000 }); + return throwError('Ignore Me!'); + }), + ); + } + + onSend(row: AppNotification) { + return this.notificationService.send(row.id).pipe( + tap(_ => { + this.snack.open('Notification Sent!', 'OK', { duration: 5000 }); + }), + catchError(error => { + this.snack.open(error, 'OK', { duration: 10000 }); + return throwError('Ignore Me!'); + }), + ); + } + + // required to override + getNewEntity(): AppNotification { + const entity = new AppNotification(); + entity.native = true; + entity.target = 'all'; + entity.targetType = TargetType.ALL; + entity.color = NotificationColor.WARN; + entity.icon = NotificationIcon.NOTIFICATIONS; + return entity; + } + + // optional + showDetails(entity: AppNotification) { + if (entity) { + this.store.dispatch(new Navigate([`/admin/notifications/${entity.id}`])); + } else { + this.store.dispatch(new Navigate(['/admin/notifications'])); + } + } + + // filterPredicate(entity: AppNotification, _filter: string): boolean { + // return entity.first_name.indexOf(_filter) !== -1; + // } + + // optional + openPopUp(entity: AppNotification) { + let isNew = false; + let id; + if (!entity) { + isNew = true; + entity = this.getNewEntity(); + } else { + id = entity.id; + } + const title = isNew ? 'Add Notification' : 'Update Notification'; + + const dialogRef = this.dialog.open(this.formRef, { + width: '720px', + disableClose: true, + data: { title: title, payload: entity }, + }); + + dialogRef + .afterClosed() + .pipe( + filter(res => res !== false), + // tap(res => console.log(res)), + concatMap((res: AppNotification) => super.updateOrCreate(res, id)), + ) + .subscribe( + _ => { + this.snack.open(isNew ? 'Notification Created!' : 'Notification Updated!', 'OK', { duration: 5000 }); + this.store.dispatch(new Navigate(['/admin/notifications'])); + }, + error => this.snack.open(error, 'OK', { duration: 10000 }), + ); + } +} diff --git a/libs/admin/src/lib/containers/overview/overview.component.html b/libs/admin/src/lib/containers/overview/overview.component.html new file mode 100644 index 000000000..c35b8ab0c --- /dev/null +++ b/libs/admin/src/lib/containers/overview/overview.component.html @@ -0,0 +1,14 @@ +
+
+ Overview +
+ + +
+ Dashboard +
+
diff --git a/libs/admin/src/lib/containers/overview/overview.component.scss b/libs/admin/src/lib/containers/overview/overview.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/containers/overview/overview.component.spec.ts b/libs/admin/src/lib/containers/overview/overview.component.spec.ts new file mode 100644 index 000000000..4f21e928d --- /dev/null +++ b/libs/admin/src/lib/containers/overview/overview.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OverviewComponent } from './overview.component'; + +describe('OverviewComponent', () => { + let component: OverviewComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [OverviewComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(OverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/containers/overview/overview.component.ts b/libs/admin/src/lib/containers/overview/overview.component.ts new file mode 100644 index 000000000..c9b90f04f --- /dev/null +++ b/libs/admin/src/lib/containers/overview/overview.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'ngx-overview', + templateUrl: './overview.component.html', + styleUrls: ['./overview.component.scss'], +}) +export class OverviewComponent implements OnInit { + constructor() {} + + ngOnInit() {} +} diff --git a/libs/admin/src/lib/containers/subscriptions/cell.templates.html b/libs/admin/src/lib/containers/subscriptions/cell.templates.html new file mode 100644 index 000000000..0a1f6457a --- /dev/null +++ b/libs/admin/src/lib/containers/subscriptions/cell.templates.html @@ -0,0 +1,5 @@ + + + diff --git a/libs/admin/src/lib/containers/subscriptions/subscriptions.component.html b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/containers/subscriptions/subscriptions.component.scss b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/admin/src/lib/containers/subscriptions/subscriptions.component.spec.ts b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.spec.ts new file mode 100644 index 000000000..6894ceb70 --- /dev/null +++ b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.spec.ts @@ -0,0 +1,24 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubscriptionsComponent } from './subscriptions.component'; + +describe('SubscriptionsComponent', () => { + let component: SubscriptionsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [SubscriptionsComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SubscriptionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/containers/subscriptions/subscriptions.component.ts b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.ts new file mode 100644 index 000000000..d945334ee --- /dev/null +++ b/libs/admin/src/lib/containers/subscriptions/subscriptions.component.ts @@ -0,0 +1,89 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { Navigate } from '@ngxs/router-plugin'; +import { MatDialog, MatSnackBar } from '@angular/material'; +import { throwError } from 'rxjs'; +import { catchError, filter, mergeMap, tap } from 'rxjs/operators'; +import { formatDistance } from 'date-fns/esm'; +import { AppConfirmService } from '@ngx-starter-kit/app-confirm'; +import { EntitiesComponent, EntityColumnDef } from '@ngx-starter-kit/shared'; +import { SubscriptionService } from '../../services/subscription.service'; +import { Subscription } from '../../models/subscription.model'; + +const entityHtmlTpl = require('../../../../../shared/src/lib/containers/entity/entity.component.html'); +const cellHtmlTpl = require('./cell.templates.html'); + +// tslint:disable-next-line +@Component({ + selector: 'ngx-subscriptions', + template: entityHtmlTpl + cellHtmlTpl, + // templateUrl: '../../../../../shared/src/lib/containers/entity/entity.component.html', + styleUrls: [ + './subscriptions.component.scss', + '../../../../../shared/src/lib/containers/entity/entity.component.scss', + ], +}) +export class SubscriptionsComponent extends EntitiesComponent { + @ViewChild('deleteButton') deleteTpl: TemplateRef; + columns: EntityColumnDef[]; + + // optional + readonly showColumnFilter = true; + readonly showToolbar = true; + + constructor( + subscriptionService: SubscriptionService, + private store: Store, + private dialog: MatDialog, + private snack: MatSnackBar, + private confirmService: AppConfirmService, + ) { + super(subscriptionService); + } + + // tslint:disable-next-line + ngOnInit() { + super.ngOnInit(); + this.columns = [ + new EntityColumnDef({ property: 'id', header: 'No.' }), + new EntityColumnDef({ property: 'userId', header: 'User' }), + new EntityColumnDef({ property: 'topics', header: 'Topics' }), + new EntityColumnDef({ + property: 'createdAt', + header: 'Created', + displayFn: entity => `${formatDistance(entity.createdAt, new Date(), { addSuffix: true })}`, + }), + new EntityColumnDef({ + property: 'updatedAt', + header: 'Updated', + displayFn: entity => `${formatDistance(entity.updatedAt, new Date(), { addSuffix: true })}`, + }), + new EntityColumnDef({ property: 'actions', header: 'Actions', template: this.deleteTpl }), + ] as EntityColumnDef[]; + } + + // optional + delete(item: Subscription) { + return this.confirmService.confirm('Confirm', `Delete Sub(${item.id}) for ${item.userId}?`).pipe( + filter(confirmed => confirmed === true), + mergeMap(_ => super.delete(item)), + tap(_ => { + this.snack.open('Subscription Deleted!', 'OK', { duration: 5000 }); + this.store.dispatch(new Navigate([`/admin/subscriptions`])); + }), + catchError(error => { + this.snack.open(error, 'OK', { duration: 10000 }); + return throwError('Ignore Me!'); + }), + ); + } + + // optional + showDetails(entity: Subscription) { + if (entity) { + this.store.dispatch(new Navigate([`/admin/subscriptions/${entity.id}`])); + } else { + this.store.dispatch(new Navigate(['/admin/subscriptions'])); + } + } +} diff --git a/libs/admin/src/lib/models/subscription.model.ts b/libs/admin/src/lib/models/subscription.model.ts new file mode 100644 index 000000000..489279705 --- /dev/null +++ b/libs/admin/src/lib/models/subscription.model.ts @@ -0,0 +1,12 @@ +import { Entity } from '@ngx-starter-kit/shared'; + +export class Subscription extends Entity { + id: number; + endpoint: string; + auth: string; + p256dh: string; + userId: string; + topics: string[]; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/libs/admin/src/lib/services/notification.service.spec.ts b/libs/admin/src/lib/services/notification.service.spec.ts new file mode 100644 index 000000000..698c4215b --- /dev/null +++ b/libs/admin/src/lib/services/notification.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: NotificationService = TestBed.get(NotificationService); + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/services/notification.service.ts b/libs/admin/src/lib/services/notification.service.ts new file mode 100644 index 000000000..552f1eb9a --- /dev/null +++ b/libs/admin/src/lib/services/notification.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { catchError, finalize, map, retry } from 'rxjs/operators'; +import { EntityService } from '@ngx-starter-kit/shared'; +import { environment } from '@env/environment'; +import { AppNotification } from '@ngx-starter-kit/notifications'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationService extends EntityService { + // Optionally overwrite baseUrl + public baseUrl = environment.API_BASE_URL; + readonly entityPath = 'notifications'; + + constructor(httpClient: HttpClient) { + super(httpClient); + } + + getAll(): Observable { + this.loadingSubject.next(true); + return this.httpClient.get<[AppNotification[], number]>(`${this.baseUrl}/${this.entityPath}`).pipe( + retry(3), // retry a failed request up to 3 times + catchError(this.handleError), + finalize(() => this.loadingSubject.next(false)), + // return without count + map(data => data[0]), + ); + } + + delete(id: number | string) { + this.loadingSubject.next(true); + return this.httpClient.delete(`${this.baseUrl}/${this.entityPath}/${id}`).pipe( + catchError(this.handleError), + finalize(() => this.loadingSubject.next(false)), + ); + } + + send(id: number | string) { + this.loadingSubject.next(true); + return this.httpClient.post(`${this.baseUrl}/${this.entityPath}/send`, { id }).pipe( + catchError(this.handleError), + finalize(() => this.loadingSubject.next(false)), + ); + } +} diff --git a/libs/admin/src/lib/services/subscription.service.spec.ts b/libs/admin/src/lib/services/subscription.service.spec.ts new file mode 100644 index 000000000..d73be796f --- /dev/null +++ b/libs/admin/src/lib/services/subscription.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { SubscriptionService } from './subscription.service'; + +describe('SubscriptionService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: SubscriptionService = TestBed.get(SubscriptionService); + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/admin/src/lib/services/subscription.service.ts b/libs/admin/src/lib/services/subscription.service.ts new file mode 100644 index 000000000..16ab9ba11 --- /dev/null +++ b/libs/admin/src/lib/services/subscription.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { catchError, finalize, map, retry } from 'rxjs/operators'; +import { EntityService } from '@ngx-starter-kit/shared'; +import { environment } from '@env/environment'; +import { Subscription } from '../models/subscription.model'; + +@Injectable({ + providedIn: 'root', +}) +export class SubscriptionService extends EntityService { + // Optionally overwrite baseUrl + public baseUrl = environment.API_BASE_URL; + readonly entityPath = 'subscription'; + + constructor(httpClient: HttpClient) { + super(httpClient); + } + + getAll(): Observable { + this.loadingSubject.next(true); + return this.httpClient.get<[Subscription[], number]>(`${this.baseUrl}/${this.entityPath}`).pipe( + retry(3), // retry a failed request up to 3 times + catchError(this.handleError), + finalize(() => this.loadingSubject.next(false)), + // return without count + map(data => data[0]), + ); + } +} diff --git a/libs/admin/src/test-setup.ts b/libs/admin/src/test-setup.ts new file mode 100644 index 000000000..8d88704e8 --- /dev/null +++ b/libs/admin/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular'; diff --git a/libs/admin/tsconfig.lib.json b/libs/admin/tsconfig.lib.json new file mode 100644 index 000000000..9d40890aa --- /dev/null +++ b/libs/admin/tsconfig.lib.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/libs/admin", + "target": "es2015", + "module": "es2015", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": ["src/test.ts", "**/*.spec.ts"] +} diff --git a/libs/admin/tsconfig.spec.json b/libs/admin/tsconfig.spec.json new file mode 100644 index 000000000..36db99af3 --- /dev/null +++ b/libs/admin/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/libs/admin", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/admin/tslint.json b/libs/admin/tslint.json new file mode 100644 index 000000000..efe6efd98 --- /dev/null +++ b/libs/admin/tslint.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "ngx", "camelCase"], + "component-selector": [true, "element", "ngx", "kebab-case"] + } +} diff --git a/libs/auth/src/index.ts b/libs/auth/src/index.ts index 292173134..c233102e9 100644 --- a/libs/auth/src/index.ts +++ b/libs/auth/src/index.ts @@ -4,4 +4,5 @@ export { AuthModule } from './lib/auth.module'; export * from './lib/auth.actions'; export * from './lib/auth.guard'; +export * from './lib/admin.guard'; export * from './lib/auth.state'; diff --git a/libs/auth/src/lib/admin.guard.spec.ts b/libs/auth/src/lib/admin.guard.spec.ts new file mode 100644 index 000000000..e5207d673 --- /dev/null +++ b/libs/auth/src/lib/admin.guard.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, async, inject } from '@angular/core/testing'; + +import { AdminGuard } from './admin.guard'; + +describe('AdminGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AdminGuard], + }); + }); + + it('should ...', inject([AdminGuard], (guard: AdminGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/libs/auth/src/lib/admin.guard.ts b/libs/auth/src/lib/admin.guard.ts new file mode 100644 index 000000000..718525749 --- /dev/null +++ b/libs/auth/src/lib/admin.guard.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { MatSnackBar } from '@angular/material'; + +@Injectable({ + providedIn: 'root', +}) +export class AdminGuard implements CanActivate { + constructor(private snack: MatSnackBar, private oauthService: OAuthService) {} + + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable | Promise | boolean { + if ((this.oauthService.hasValidIdToken() || this.oauthService.hasValidAccessToken()) && this.isAdmin()) { + return true; + } else { + if ((this.oauthService.hasValidIdToken() || this.oauthService.hasValidAccessToken()) && this.isAdmin()) { + return true; + } else { + this.snack.open('You are not Admin. Please login with kubeadmin', 'OK', { duration: 5000 }); + return false; + } + } + } + + isAdmin(): boolean { + // const userRoles = (this.oauthService.getIdentityClaims()).groups.filter(role => role.startsWith('NGX_')); + return (this.oauthService.getIdentityClaims()).preferred_username === 'kubeadmin'; + } +} diff --git a/libs/core/src/lib/core.module.ts b/libs/core/src/lib/core.module.ts index d2409cd40..f96548258 100644 --- a/libs/core/src/lib/core.module.ts +++ b/libs/core/src/lib/core.module.ts @@ -55,7 +55,7 @@ library.add(faTwitter, faGithub, faGoogle); MatSnackBarModule, NgxPageScrollModule, NavigatorModule.forRoot(defaultMenu), - NgxsModule.forRoot([AuthState, MenuState, PreferenceState, AppState]), + NgxsModule.forRoot([AuthState, MenuState, PreferenceState, AppState], { developmentMode: !environment.production }), NgxsStoragePluginModule.forRoot({ key: ['preference', 'app.installed'], }), diff --git a/libs/core/src/lib/menu-data.ts b/libs/core/src/lib/menu-data.ts index e4040a81c..bbd10660f 100644 --- a/libs/core/src/lib/menu-data.ts +++ b/libs/core/src/lib/menu-data.ts @@ -11,6 +11,11 @@ export const defaultMenu: MenuItem[] = [ icon: 'dashboard', link: '/dashboard', }, + { + name: 'Admin', + icon: 'security', + link: '/admin', + }, // { // name: 'Custom components', // type: SidenavItemType.Separator, @@ -161,6 +166,23 @@ export const defaultMenu: MenuItem[] = [ ]; export const adminMenu: MenuItem[] = [ + { + name: 'Messaging', + icon: 'business', + disabled: false, + children: [ + { + name: 'Subscriptions', + icon: 'web_aaset', + link: '/admin/subscriptions', + }, + { + name: 'Notifications', + icon: 'grid_on', + link: '/admin/notifications', + }, + ], + }, { name: 'Dashboard', type: MenuItemType.DropDown, diff --git a/nx.json b/nx.json index ff463ee83..6e040d629 100644 --- a/nx.json +++ b/nx.json @@ -9,126 +9,216 @@ }, "projects": { "webapp": { - "tags": ["app-module"] + "tags": [ + "app-module" + ] }, "webapp-e2e": { "tags": [], - "implicitDependencies": ["webapp"] + "implicitDependencies": [ + "webapp" + ] }, "api": { - "tags": ["api-module"] + "tags": [ + "api-module" + ] }, "api-e2e": { "tags": [], - "implicitDependencies": ["api"] + "implicitDependencies": [ + "api" + ] }, "backend": { - "tags": ["api-module"] + "tags": [ + "api-module" + ] }, "navigator": { - "tags": ["private-module", "core-module"] + "tags": [ + "private-module", + "core-module" + ] }, "socketio-plugin": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "tree": { - "tags": ["utils"] + "tags": [ + "utils" + ] }, "home": { - "tags": ["layout", "entry-module"] + "tags": [ + "layout", + "entry-module" + ] }, "dashboard": { - "tags": ["layout", "entry-module"] + "tags": [ + "layout", + "entry-module" + ] }, "not-found": { - "tags": ["entry-module"] + "tags": [ + "entry-module" + ] }, "experiments": { - "tags": ["child-module"] + "tags": [ + "child-module" + ] }, "widgets": { - "tags": ["child-module"] + "tags": [ + "child-module" + ] }, "material": { - "tags": ["shared-module"] + "tags": [ + "shared-module" + ] }, "animations": { - "tags": ["utils"] + "tags": [ + "utils" + ] }, "utils": { - "tags": ["utils"] + "tags": [ + "utils" + ] }, "auth": { - "tags": ["private-module", "core-module"] + "tags": [ + "private-module", + "core-module" + ] }, "shared": { - "tags": ["shared-module"] + "tags": [ + "shared-module" + ] }, "app-confirm": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "draggable": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "breadcrumbs": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "scroll-to-top": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "scrollbar": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "context-menu": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "theme-picker": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "quickpanel": { - "tags": ["private-module"] + "tags": [ + "private-module" + ] }, "svg-viewer": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "led": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "chat-box": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "json-diff": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "toolbar": { - "tags": ["private-module"] + "tags": [ + "private-module" + ] }, "sidenav": { - "tags": ["private-module"] + "tags": [ + "private-module" + ] }, "loading-overlay": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "grid": { - "tags": ["child-module"] + "tags": [ + "child-module" + ] }, "notifications": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "core": { - "tags": ["core-module"] + "tags": [ + "core-module" + ] }, "clap": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "image-comparison": { - "tags": ["public-module"] + "tags": [ + "public-module" + ] }, "models": { - "tags": ["utils"] + "tags": [ + "utils" + ] }, "ngx-utils": { - "tags": ["public-module", "utils"] + "tags": [ + "public-module", + "utils" + ] + }, + "admin": { + "tags": [ + "entry-module" + ] } } }