diff --git a/src/app/account/account.component.ts b/src/app/account/account.component.ts index e01b5f8ce..d8789059b 100644 --- a/src/app/account/account.component.ts +++ b/src/app/account/account.component.ts @@ -14,7 +14,6 @@ import { MaybeNull } from "src/app/common/app.types"; import { AccountNotFoundError } from "src/app/common/errors"; import { AccountPageQueryParams } from "./account.component.model"; import { ModalService } from "../components/modal/modal.service"; -import { LoggedUserService } from "../auth/logged-user.service"; @Component({ selector: "app-account", @@ -35,7 +34,6 @@ export class AccountComponent extends BaseComponent implements OnInit { private route = inject(ActivatedRoute); private modalService = inject(ModalService); private accountService = inject(AccountService); - private loggedUserService = inject(LoggedUserService); public ngOnInit(): void { const accountName$ = this.route.params.pipe( diff --git a/src/app/admin-view/admin-link/admin-link.component.html b/src/app/admin-view/admin-link/admin-link.component.html new file mode 100644 index 000000000..5abfed52c --- /dev/null +++ b/src/app/admin-view/admin-link/admin-link.component.html @@ -0,0 +1,2 @@ + + admin_panel_settings diff --git a/src/app/admin-view/admin-link/admin-link.component.scss b/src/app/admin-view/admin-link/admin-link.component.scss new file mode 100644 index 000000000..07d6da3f0 --- /dev/null +++ b/src/app/admin-view/admin-link/admin-link.component.scss @@ -0,0 +1,25 @@ +:host { + position: relative; +} + +.admin-privileges-icon { + font-size: 16px; + position: absolute; + top: -4px; + right: -27px; + display: block; +} + +.light-theme { + color: #f1f1f1 !important; +} + +.dark-theme { + color: #378bb0 !important; +} + +@media (width <=768px) { + .admin-privileges-icon { + display: none; + } +} diff --git a/src/app/admin-view/admin-link/admin-link.component.spec.ts b/src/app/admin-view/admin-link/admin-link.component.spec.ts new file mode 100644 index 000000000..a0d747274 --- /dev/null +++ b/src/app/admin-view/admin-link/admin-link.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminLinkComponent } from './admin-link.component'; + +describe('AdminLinkComponent', () => { + let component: AdminLinkComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AdminLinkComponent] + }); + fixture = TestBed.createComponent(AdminLinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/admin-view/admin-link/admin-link.component.ts b/src/app/admin-view/admin-link/admin-link.component.ts new file mode 100644 index 000000000..19c85c72f --- /dev/null +++ b/src/app/admin-view/admin-link/admin-link.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; + +@Component({ + selector: "app-admin-link", + templateUrl: "./admin-link.component.html", + styleUrls: ["./admin-link.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminLinkComponent { + @Input({ required: true }) adminPrivileges: boolean; + @Input() theme: string = "light-theme"; +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 3c4f6c7e2..b9557a0b8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -16,7 +16,7 @@ import { SetTransformComponent } from "./dataset-view/additional-components/meta import { LoginGuard } from "./auth/guards/login.guard"; import { ReturnToCliComponent } from "./components/return-to-cli/return-to-cli.component"; import { AddPushSourceComponent } from "./dataset-view/additional-components/metadata-component/components/source-events/add-push-source/add-push-source.component"; -import { AdminGuard } from "./auth/guards/admin.guard"; +import { adminGuard } from "./auth/guards/admin.guard"; import { AdminDashboardComponent } from "./admin-view/admin-dashboard/admin-dashboard.component"; import { DatasetFlowDetailsComponent } from "./dataset-flow/dataset-flow-details/dataset-flow-details.component"; import { AccountComponent } from "./account/account.component"; @@ -44,7 +44,7 @@ export const routes: Routes = [ component: DatasetCreateComponent, }, { - canActivate: [AdminGuard], + canActivate: [adminGuard], path: ProjectLinks.URL_ADMIN_DASHBOARD, component: AdminDashboardComponent, }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c5a17e92d..99f2525e2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,7 +17,6 @@ import { LoginService } from "./auth/login/login.service"; import { loadErrorMessages } from "@apollo/client/dev"; import { isDevMode } from "@angular/core"; import moment from "moment"; -import { LoggedUserService } from "./auth/logged-user.service"; import packageFile from "../../package.json"; import { LocalStorageService } from "./services/local-storage.service"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -58,7 +57,6 @@ export class AppComponent extends BaseComponent implements OnInit { private navigationService = inject(NavigationService); private appConfigService = inject(AppConfigService); private cdr = inject(ChangeDetectorRef); - private loggedUserService = inject(LoggedUserService); private localStorageService = inject(LocalStorageService); public ngOnInit(): void { @@ -86,6 +84,13 @@ export class AppComponent extends BaseComponent implements OnInit { this.loggedAccount = user ? _.cloneDeep(user) : AppComponent.ANONYMOUS_ACCOUNT_INFO; this.cdr.detectChanges(); }); + + this.initAdminSlideToggle(); + } + + private initAdminSlideToggle(): void { + const flag = this.localStorageService.adminPrivileges; + this.loggedUserService.emitAdminPrivilegesChanges(Boolean(flag)); } private setMomentOptions(): void { @@ -170,7 +175,7 @@ export class AppComponent extends BaseComponent implements OnInit { yesButtonText: "Ok", }), ); - //TODO: Implement AdminDashBoardComponent - // this.navigationService.navigateToAdminDashBoard(); + // TODO: Implement AdminDashBoardComponent + // this.navigationService.navigateToAdminDashBoard(); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2af380646..d0d6bf37b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -74,6 +74,7 @@ import { AccessTokensTabComponent } from "./auth/settings/tabs/access-tokens-tab import { MatSlideToggleModule } from "@angular/material/slide-toggle"; import { DynamicTableModule } from "./components/dynamic-table/dynamic-table.module"; import { AutofocusModule } from "./common/directives/autofocus.module"; +import { AdminLinkComponent } from "./admin-view/admin-link/admin-link.component"; const Services = [ { @@ -210,6 +211,7 @@ const MatModules = [ AdminDashboardComponent, AccessTokensTabComponent, AccountFlowsTabComponent, + AdminLinkComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/auth/guards/admin.guard.ts b/src/app/auth/guards/admin.guard.ts index 7d0d1a580..685f4ebea 100644 --- a/src/app/auth/guards/admin.guard.ts +++ b/src/app/auth/guards/admin.guard.ts @@ -1,23 +1,21 @@ -import { inject, Injectable } from "@angular/core"; +import { inject } from "@angular/core"; import { NavigationService } from "src/app/services/navigation.service"; import { LoggedUserService } from "../logged-user.service"; +import { CanActivateFn } from "@angular/router"; +import { combineLatest, map, of } from "rxjs"; -@Injectable({ - providedIn: "root", -}) -export class AdminGuard { - private navigationService = inject(NavigationService); - private loggedUserService = inject(LoggedUserService); +export const adminGuard: CanActivateFn = () => { + const navigationService = inject(NavigationService); + const loggedUserService = inject(LoggedUserService); - public canActivate(): boolean { - if (!this.isAdmin()) { - this.navigationService.navigateToHome(); - return false; - } - return true; - } - - private isAdmin(): boolean { - return this.loggedUserService.isAdmin; - } -} + return combineLatest([of(loggedUserService.isAdmin), loggedUserService.adminPrivilegesChanges]).pipe( + map(([isAdmin, adminPrivileges]) => { + if (isAdmin && adminPrivileges.value) { + return true; + } else { + navigationService.navigateToHome(); + return false; + } + }), + ); +}; diff --git a/src/app/auth/logged-user.service.ts b/src/app/auth/logged-user.service.ts index 5e010b6ee..7b29be04d 100644 --- a/src/app/auth/logged-user.service.ts +++ b/src/app/auth/logged-user.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@angular/core"; import { catchError, first } from "rxjs/operators"; -import { EMPTY, Observable, ReplaySubject, Subject } from "rxjs"; + +import { EMPTY, BehaviorSubject, Observable, ReplaySubject, Subject } from "rxjs"; import { NavigationService } from "../services/navigation.service"; import { MaybeNull } from "../common/app.types"; import { isNull } from "lodash"; @@ -20,6 +21,9 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; export class LoggedUserService extends UnsubscribeDestroyRefAdapter { private loggedInUser: MaybeNull = null; private loggedInUser$: Subject> = new ReplaySubject>(1); + private adminPrivileges$: BehaviorSubject<{ value: boolean }> = new BehaviorSubject<{ value: boolean }>({ + value: false, + }); constructor( private loginService: LoginService, @@ -38,6 +42,14 @@ export class LoggedUserService extends UnsubscribeDestroyRefAdapter { .subscribe((user: AccountFragment) => this.changeUser(user)); } + public get adminPrivilegesChanges(): Observable<{ value: boolean }> { + return this.adminPrivileges$.asObservable(); + } + + public emitAdminPrivilegesChanges(value: boolean): void { + return this.adminPrivileges$.next({ value }); + } + public initializeCompletes(): Observable { const loginInstructions: AppLoginInstructions | null = this.appConfigService.loginInstructions; if (loginInstructions) { @@ -83,6 +95,7 @@ export class LoggedUserService extends UnsubscribeDestroyRefAdapter { this.changeUser(null); this.resetAccessToken(); this.clearGraphQLCache(); + this.resetAdminPrivileges(); } private attemptPreviousAuthenticationCompletes(): Observable { @@ -116,4 +129,9 @@ export class LoggedUserService extends UnsubscribeDestroyRefAdapter { private saveAccessToken(token: string): void { this.localStorageService.setAccessToken(token); } + + private resetAdminPrivileges(): void { + this.localStorageService.setAdminPriveleges(null); + this.emitAdminPrivilegesChanges(false); + } } diff --git a/src/app/auth/settings/account-settings.component.html b/src/app/auth/settings/account-settings.component.html index 4b2eb9ece..22950a1f2 100644 --- a/src/app/auth/settings/account-settings.component.html +++ b/src/app/auth/settings/account-settings.component.html @@ -1,9 +1,9 @@ -
+
- +
diff --git a/src/app/auth/settings/account-settings.component.scss b/src/app/auth/settings/account-settings.component.scss index fc16e95c4..d3d25c658 100644 --- a/src/app/auth/settings/account-settings.component.scss +++ b/src/app/auth/settings/account-settings.component.scss @@ -1,5 +1,21 @@ @import "var"; +:host { + ::ng-deep { + .mat-slide-toggle { + .mat-slide-toggle-ripple { + position: relative; + } + } + + .mdc-switch { + .mdc-switch__ripple { + display: none; + } + } + } +} + .p-responsive { padding: 0 60px; diff --git a/src/app/auth/settings/account-settings.component.ts b/src/app/auth/settings/account-settings.component.ts index e0e20860f..b7c3f27b3 100644 --- a/src/app/auth/settings/account-settings.component.ts +++ b/src/app/auth/settings/account-settings.component.ts @@ -3,13 +3,14 @@ import { AccountFragment } from "src/app/api/kamu.graphql.interface"; import { AccountSettingsTabs } from "./account-settings.constants"; import { ChangeDetectionStrategy, Component, inject, OnInit } from "@angular/core"; import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; -import { filter } from "rxjs/operators"; +import { filter, map } from "rxjs/operators"; import { BaseComponent } from "src/app/common/base.component"; import AppValues from "src/app/common/app.values"; import { MaybeNull, MaybeUndefined } from "src/app/common/app.types"; -import { Observable } from "rxjs"; -import { LoggedUserService } from "../logged-user.service"; +import { combineLatest, Observable } from "rxjs"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { MatSlideToggleChange } from "@angular/material/slide-toggle"; +import { LocalStorageService } from "src/app/services/local-storage.service"; @Component({ selector: "app-settings", @@ -26,7 +27,12 @@ export class AccountSettingsComponent extends BaseComponent implements OnInit { private router = inject(Router); private route = inject(ActivatedRoute); - private loggedUserService = inject(LoggedUserService); + private localStorageService = inject(LocalStorageService); + + public userData$: Observable<{ + user: AccountFragment; + adminPrivileges: boolean; + } | null>; public ngOnInit(): void { this.router.events @@ -39,13 +45,35 @@ export class AccountSettingsComponent extends BaseComponent implements OnInit { }); this.extractActiveTabFromRoute(); - this.user$ = this.loggedUserService.loggedInUserChanges; + + this.userData$ = combineLatest([ + this.loggedUserService.loggedInUserChanges, + this.loggedUserService.adminPrivilegesChanges, + ]).pipe( + map(([user, adminPrivileges]) => { + return user + ? { + user, + adminPrivileges: adminPrivileges.value, + } + : null; + }), + ); } public getRouteLink(tab: AccountSettingsTabs): string { return `/${ProjectLinks.URL_SETTINGS}/${tab}`; } + public get isAdmin(): boolean { + return this.loggedUserService.isAdmin; + } + + public adminSlideToggleChange(event: MatSlideToggleChange): void { + this.loggedUserService.emitAdminPrivilegesChanges(event.checked); + this.localStorageService.setAdminPriveleges(event.checked); + } + private extractActiveTabFromRoute(): void { const categoryParam: MaybeUndefined = this.route.snapshot.params[ ProjectLinks.URL_PARAM_CATEGORY diff --git a/src/app/common/app.types.ts b/src/app/common/app.types.ts index a43eec10a..c03b27e34 100644 --- a/src/app/common/app.types.ts +++ b/src/app/common/app.types.ts @@ -1,3 +1,14 @@ export type MaybeNull = T | null; export type MaybeUndefined = T | undefined; export type MaybeNullOrUndefined = T | null | undefined; + +export interface AdminAvailableButtonType { + label: string; + icon: string; + datasetId: string; + visible?: boolean; + showAdminIcon?: boolean; + adminPrivileges?: boolean; + disabled?: boolean; + class?: string; +} diff --git a/src/app/common/app.values.ts b/src/app/common/app.values.ts index ab41e3817..1e70737cb 100644 --- a/src/app/common/app.values.ts +++ b/src/app/common/app.values.ts @@ -8,6 +8,8 @@ export default class AppValues { public static readonly LOCAL_STORAGE_LOGIN_CALLBACK_URL = "login_callback_url"; public static readonly LOCAL_STORAGE_LOGIN_REDIRECT_URL = "login_redirect_url"; public static readonly LOCAL_STORAGE_ACCOUNT_ID = "account_id"; + public static readonly LOCAL_STORAGE_ADMIN_PRIVILEGES = "admin_priveleges"; + public static readonly SESSION_STORAGE_SIDE_PANEL_VISIBLE = "side_panel_visible"; public static readonly DEFAULT_USER_DISPLAY_NAME = "anonymous"; public static readonly DEFAULT_AVATAR_URL = "https://avatars.githubusercontent.com/u/11951648?v=4"; diff --git a/src/app/common/base.component.ts b/src/app/common/base.component.ts index 964617044..3027747d2 100644 --- a/src/app/common/base.component.ts +++ b/src/app/common/base.component.ts @@ -5,9 +5,12 @@ import ProjectLinks from "../project-links"; import { requireValue } from "./app.helpers"; import { UnsubscribeDestroyRefAdapter } from "./unsubscribe.ondestroy.adapter"; import { Observable, map } from "rxjs"; +import { LoggedUserService } from "../auth/logged-user.service"; export abstract class BaseComponent extends UnsubscribeDestroyRefAdapter { + public adminPrivileges$: Observable<{ value: boolean }>; protected activatedRoute = inject(ActivatedRoute); + protected loggedUserService = inject(LoggedUserService); public get searchString(): string { return window.location.search; diff --git a/src/app/common/components/admin-available-button/admin-available-button.component.html b/src/app/common/components/admin-available-button/admin-available-button.component.html new file mode 100644 index 000000000..5e7cdbbc1 --- /dev/null +++ b/src/app/common/components/admin-available-button/admin-available-button.component.html @@ -0,0 +1,5 @@ + diff --git a/src/app/common/components/admin-available-button/admin-available-button.component.scss b/src/app/common/components/admin-available-button/admin-available-button.component.scss new file mode 100644 index 000000000..d7f8faa6c --- /dev/null +++ b/src/app/common/components/admin-available-button/admin-available-button.component.scss @@ -0,0 +1,7 @@ +mat-icon.admin-icon { + color: #378bb0; + transform: scale(0.65); + position: relative; + bottom: 4px; + right: 6px; +} diff --git a/src/app/common/components/admin-available-button/admin-available-button.component.spec.ts b/src/app/common/components/admin-available-button/admin-available-button.component.spec.ts new file mode 100644 index 000000000..4a5369373 --- /dev/null +++ b/src/app/common/components/admin-available-button/admin-available-button.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminAvailableButtonComponent } from './admin-available-button.component'; + +describe('AdminAvailableButtonComponent', () => { + let component: AdminAvailableButtonComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [AdminAvailableButtonComponent] + }); + fixture = TestBed.createComponent(AdminAvailableButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/common/components/admin-available-button/admin-available-button.component.ts b/src/app/common/components/admin-available-button/admin-available-button.component.ts new file mode 100644 index 000000000..ab9e81283 --- /dev/null +++ b/src/app/common/components/admin-available-button/admin-available-button.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core"; + +@Component({ + selector: "app-admin-available-button", + templateUrl: "./admin-available-button.component.html", + styleUrls: ["./admin-available-button.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AdminAvailableButtonComponent { + @Input() public visible: boolean = true; + @Input() public icon: string = ""; + @Input({ required: true }) public label: string; + @Input() public class?: string; + @Input() public disabled: boolean; + @Input() public datasetId: string; + @Input() public showAdwinIcon: boolean; + @Input() public adminPrivileges: boolean; + @Output() public onClick = new EventEmitter(); + + public click(): void { + this.onClick.emit(); + } +} diff --git a/src/app/components/app-header/app-header.component.html b/src/app/components/app-header/app-header.component.html index f830c269e..a7103fd13 100644 --- a/src/app/components/app-header/app-header.component.html +++ b/src/app/components/app-header/app-header.component.html @@ -88,7 +88,7 @@ class="d-flex flex-column align-items-center flex-md-row flex-self-stretch flex-md-self-auto width-full justify-content-between" aria-label="Global" > -
+