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 c72c7ae8e..1f635718d 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -15,7 +15,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"; @@ -42,7 +42,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 047a3c7c4..1ab9affe9 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"; @@ -63,7 +62,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 { @@ -91,6 +89,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 { @@ -175,7 +180,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 946a90110..7842b0bef 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -73,6 +73,7 @@ import { AccountComponent } from "./account/account.component"; import { DatasetsTabComponent } from "./account/additional-components/datasets-tab/datasets-tab.component"; import { AccessTokensTabComponent } from "./auth/settings/tabs/access-tokens-tab/access-tokens-tab.component"; import { MatSlideToggleModule } from "@angular/material/slide-toggle"; +import { AdminLinkComponent } from "./admin-view/admin-link/admin-link.component"; const Services = [ { @@ -204,6 +205,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 1e2348068..895b84a31 100644 --- a/src/app/auth/logged-user.service.ts +++ b/src/app/auth/logged-user.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { catchError, first } from "rxjs/operators"; -import { Observable, ReplaySubject, Subject, of } from "rxjs"; +import { BehaviorSubject, Observable, ReplaySubject, Subject, of } from "rxjs"; import { NavigationService } from "../services/navigation.service"; import { MaybeNull } from "../common/app.types"; import { isNull } from "lodash"; @@ -20,6 +20,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 +41,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: AppConfigLoginInstructions | null = this.appConfigService.loginInstructions; if (loginInstructions) { @@ -83,6 +94,7 @@ export class LoggedUserService extends UnsubscribeDestroyRefAdapter { this.changeUser(null); this.resetAccessToken(); this.clearGraphQLCache(); + this.resetAdminPrivileges(); } private attemptPreviousAuthenticationCompletes(): Observable { @@ -116,4 +128,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 eac7ba5e3..961a4348e 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.values.ts b/src/app/common/app.values.ts index baefba99c..3b60f545f 100644 --- a/src/app/common/app.values.ts +++ b/src/app/common/app.values.ts @@ -7,6 +7,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/components/app-header/app-header.component.html b/src/app/components/app-header/app-header.component.html index fc9023425..83e289ed0 100644 --- a/src/app/components/app-header/app-header.component.html +++ b/src/app/components/app-header/app-header.component.html @@ -119,13 +119,15 @@ Your Datasets - - Dashboard - + + + + + Dashboard + + + + = { root: DatasetKind.Root, diff --git a/src/app/dataset-view/additional-components/metadata-component/components/final-yaml-modal/final-yaml-modal.component.ts b/src/app/dataset-view/additional-components/metadata-component/components/final-yaml-modal/final-yaml-modal.component.ts index 21e10d0e1..0ad73619d 100644 --- a/src/app/dataset-view/additional-components/metadata-component/components/final-yaml-modal/final-yaml-modal.component.ts +++ b/src/app/dataset-view/additional-components/metadata-component/components/final-yaml-modal/final-yaml-modal.component.ts @@ -3,7 +3,6 @@ import { ChangeDetectionStrategy, Component, inject, Input } from "@angular/core import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap"; import { DatasetInfo } from "src/app/interface/navigation.interface"; import { DatasetCommitService } from "../../../overview-component/services/dataset-commit.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @Component({ @@ -18,7 +17,6 @@ export class FinalYamlModalComponent extends BaseComponent { public activeModal = inject(NgbActiveModal); private datasetCommitService = inject(DatasetCommitService); - private loggedUserService = inject(LoggedUserService); public saveEvent(): void { this.datasetCommitService diff --git a/src/app/dataset-view/additional-components/overview-component/components/edit-details-modal/edit-details-modal.component.ts b/src/app/dataset-view/additional-components/overview-component/components/edit-details-modal/edit-details-modal.component.ts index da391b6f3..152e38cd6 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/edit-details-modal/edit-details-modal.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/components/edit-details-modal/edit-details-modal.component.ts @@ -12,7 +12,6 @@ import { BaseComponent } from "src/app/common/base.component"; import { DatasetSchema, DataRow } from "src/app/interface/dataset.interface"; import { TemplatesYamlEventsService } from "src/app/services/templates-yaml-events.service"; import { DatasetCommitService } from "../../services/dataset-commit.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import { finalize } from "rxjs"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -36,7 +35,6 @@ export class EditDetailsModalComponent extends BaseComponent implements OnInit { private datasetCommitService = inject(DatasetCommitService); private yamlEventService = inject(TemplatesYamlEventsService); public activeModal = inject(NgbActiveModal); - private loggedUserService = inject(LoggedUserService); public get keywords(): string[] { return Array.from(this.keywordsSet); diff --git a/src/app/dataset-view/additional-components/overview-component/components/edit-license-modal/edit-license-modal.component.ts b/src/app/dataset-view/additional-components/overview-component/components/edit-license-modal/edit-license-modal.component.ts index be343cf77..c2b57e9d4 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/edit-license-modal/edit-license-modal.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/components/edit-license-modal/edit-license-modal.component.ts @@ -13,7 +13,6 @@ import { MaybeNull } from "src/app/common/app.types"; import { DatasetSchema, DataRow } from "src/app/interface/dataset.interface"; import { TemplatesYamlEventsService } from "src/app/services/templates-yaml-events.service"; import { DatasetCommitService } from "../../services/dataset-commit.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import { finalize } from "rxjs"; import { LicenseFormType } from "./edit-license-modal.types"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -27,7 +26,6 @@ export class EditLicenseModalComponent extends BaseComponent implements OnInit { private fb = inject(FormBuilder); private datasetCommitService = inject(DatasetCommitService); private yamlEventService = inject(TemplatesYamlEventsService); - private loggedUserService = inject(LoggedUserService); @Input({ required: true }) public datasetBasics: DatasetBasicsFragment; @Input({ required: true }) public currentState?: { diff --git a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts index db6316bc9..f57163f91 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/components/edit-watermark-modal/edit-watermark-modal.component.ts @@ -7,7 +7,6 @@ import { DatasetBasicsFragment } from "src/app/api/kamu.graphql.interface"; import { BaseComponent } from "src/app/common/base.component"; import { MY_MOMENT_FORMATS } from "src/app/common/data.helpers"; import { DatasetCommitService } from "../../services/dataset-commit.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import { finalize } from "rxjs"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -26,7 +25,6 @@ export class EditWatermarkModalComponent extends BaseComponent implements OnInit public activeModal = inject(NgbActiveModal); private datasetCommitService = inject(DatasetCommitService); - private loggedUserService = inject(LoggedUserService); ngOnInit(): void { this.date = new Date(); diff --git a/src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.ts b/src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.ts index cb852108b..f3bd57c70 100644 --- a/src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.ts @@ -4,7 +4,6 @@ import { MaybeNull } from "src/app/common/app.types"; import { BaseComponent } from "src/app/common/base.component"; import { EditMode } from "./readme-section.types"; import { DatasetCommitService } from "../../services/dataset-commit.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @Component({ @@ -29,7 +28,6 @@ export class ReadmeSectionComponent extends BaseComponent implements OnInit { } private datasetCommitService = inject(DatasetCommitService); - private loggedUserService = inject(LoggedUserService); public ngOnInit(): void { if (this.currentReadme) { diff --git a/src/app/dataset-view/additional-components/overview-component/overview.component.ts b/src/app/dataset-view/additional-components/overview-component/overview.component.ts index 41de007f2..a47d71162 100644 --- a/src/app/dataset-view/additional-components/overview-component/overview.component.ts +++ b/src/app/dataset-view/additional-components/overview-component/overview.component.ts @@ -32,7 +32,6 @@ import { promiseWithCatch } from "src/app/common/app.helpers"; import { ModalService } from "src/app/components/modal/modal.service"; import AppValues from "src/app/common/app.values"; import { FileUploadService } from "src/app/services/file-upload.service"; -import { LoggedUserService } from "src/app/auth/logged-user.service"; import ProjectLinks from "src/app/project-links"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -69,7 +68,6 @@ export class OverviewComponent extends BaseComponent implements OnInit { private fileUploadService = inject(FileUploadService); private configService = inject(AppConfigService); private modalService = inject(ModalService); - private loggedUserService = inject(LoggedUserService); public ngOnInit(): void { this.uploadFileLoading$ = this.fileUploadService.isUploadFile; diff --git a/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.html b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.html new file mode 100644 index 000000000..2a2008eee --- /dev/null +++ b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.html @@ -0,0 +1,17 @@ + + +
+ {{ icon }} + {{ label }} + + admin_panel_settings +
+
+ diff --git a/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.scss b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.scss new file mode 100644 index 000000000..f6040efce --- /dev/null +++ b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.scss @@ -0,0 +1,42 @@ +:host { + mat-button-toggle { + padding: 0 0 13px !important; + border: 2px transparent solid !important; + background: inherit !important; + + &.active-link { + border-bottom-color: #000 !important; + border-radius: 0; + } + + &:not(.active-link) { + &:hover { + padding: 0 0 14px !important; + box-shadow: none !important; + border-bottom: 1px solid !important; + border-radius: 0 !important; + } + } + } +} + +::ng-deep { + .mat-button-toggle-disabled { + .mat-button-toggle-button { + cursor: not-allowed; + color: rgb(0 0 0 / 51%); + + a { + cursor: not-allowed; + color: rgb(0 0 0 / 51%); + pointer-events: none; + } + } + } +} + +.admin-icon { + font-size: 16px; + bottom: 5px; + color: #378bb0; +} diff --git a/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.spec.ts b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.spec.ts new file mode 100644 index 000000000..df5886056 --- /dev/null +++ b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatasetViewMenuItemComponent } from './dataset-view-menu-item.component'; + +describe('DatasetViewMenuItemComponent', () => { + let component: DatasetViewMenuItemComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DatasetViewMenuItemComponent] + }); + fixture = TestBed.createComponent(DatasetViewMenuItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.ts b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.ts new file mode 100644 index 000000000..696ffbc5b --- /dev/null +++ b/src/app/dataset-view/dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; +import { MaybeUndefined } from "src/app/common/app.types"; + +@Component({ + selector: "app-dataset-view-menu-item", + templateUrl: "./dataset-view-menu-item.component.html", + styleUrls: ["./dataset-view-menu-item.component.scss"], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DatasetViewMenuItemComponent { + @Input({ required: true }) public dataTestId: string; + @Input({ required: true }) public activeLink: boolean; + @Input({ required: true }) public value: string; + @Input({ required: true }) public label: string; + @Input({ required: true }) public icon: string; + @Input({ required: true }) public datasetLink: string; + @Input({ required: true }) public adminPrivileges: boolean | null; + @Input({ required: true }) public visible: boolean; + @Input() public disabled: MaybeUndefined; + @Input() public showAdminIcon: MaybeUndefined; +} diff --git a/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.html b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.html index 2ab71dc90..0eb5cd2f9 100644 --- a/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.html +++ b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.html @@ -3,97 +3,25 @@ [style]="isMinimizeSearchAdditionalButtons ? 'grid-template-columns: 1fr;' : ''" id="container-dataset-tabs" #datasetViewMenu + *ngIf="adminPrivileges$ | async as adminPrivileges" > diff --git a/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.ts b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.ts index 4f5f08420..12694c3d9 100644 --- a/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.ts +++ b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.component.ts @@ -15,7 +15,9 @@ import { DatasetViewTypeEnum } from "../dataset-view.interface"; import { SideNavHelper } from "../../common/sidenav.helper"; import { isMobileView, promiseWithCatch } from "src/app/common/app.helpers"; import { DatasetBasicsFragment, DatasetPermissionsFragment } from "src/app/api/kamu.graphql.interface"; -import { DatasetPermissionsService } from "../dataset.permissions.service"; +import { Observable } from "rxjs"; +import { DatasetViewMenuItems } from "./dataset-view-menu.model"; +import { LoggedUserService } from "src/app/auth/logged-user.service"; @Component({ selector: "app-dataset-view-menu", @@ -32,12 +34,13 @@ export class DatasetViewMenuComponent implements OnInit, AfterViewInit { @Input({ required: true }) datasetPermissions: DatasetPermissionsFragment; @Input({ required: true }) datasetViewType: DatasetViewTypeEnum; @Input() isMinimizeSearchAdditionalButtons: boolean; - public readonly DatasetViewTypeEnum: typeof DatasetViewTypeEnum = DatasetViewTypeEnum; - + public datasetMenuItemDescriptors = DatasetViewMenuItems; + public adminPrivileges$: Observable<{ value: boolean }>; private sideNavHelper: SideNavHelper; + public readonly DatasetViewTypeEnum: typeof DatasetViewTypeEnum = DatasetViewTypeEnum; - private datasetPermissionsServices = inject(DatasetPermissionsService); private widgetHeightService = inject(WidgetHeightService); + private loggedUserService = inject(LoggedUserService); public ngAfterViewInit(): void { this.widgetHeightService.setWidgetOffsetTop( @@ -50,46 +53,11 @@ export class DatasetViewMenuComponent implements OnInit, AfterViewInit { if (this.sidenav) { this.sideNavHelper = new SideNavHelper(this.sidenav); } - } - - public get isDatasetViewTypeOverview(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Overview; - } - - public get isDatasetViewTypeData(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Data; - } - - public get isDatasetViewTypeMetadata(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Metadata; - } - - public get isDatasetViewTypeHistory(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.History; - } - - public get isDatasetViewTypeLineage(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Lineage; - } - - public get isDatasetViewTypeDiscussions(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Discussions; - } - - public get isDatasetViewTypeFlows(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Flows; - } - - public get isDatasetViewTypeSettings(): boolean { - return this.datasetViewType === DatasetViewTypeEnum.Settings; - } - - public get shouldAllowSettingsTab(): boolean { - return this.datasetPermissionsServices.shouldAllowSettingsTab(this.datasetPermissions); + this.adminPrivileges$ = this.loggedUserService.adminPrivilegesChanges; } public get datasetLink(): string { - return `/${this.datasetBasics.owner.accountName}/${this.datasetBasics.name}/`; + return `/${this.datasetBasics.owner.accountName}/${this.datasetBasics.name}`; } @HostListener("window:resize") diff --git a/src/app/dataset-view/dataset-view-menu/dataset-view-menu.model.ts b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.model.ts new file mode 100644 index 000000000..bcee100ae --- /dev/null +++ b/src/app/dataset-view/dataset-view-menu/dataset-view-menu.model.ts @@ -0,0 +1,94 @@ +import { DatasetPermissionsFragment } from "src/app/api/kamu.graphql.interface"; + +export interface DatasetMenuItemOptions { + dataTestId: string; + value: string; + label: string; + icon: string; + disabled?: boolean; + showAdminIcon?: boolean; + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment, adminPrivileges?: boolean) => boolean; +} + +export const DatasetViewMenuItems: DatasetMenuItemOptions[] = [ + { + dataTestId: "navigateToOverview", + value: "overview", + label: "Overview", + icon: "visibility", + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + { + dataTestId: "navigateToData", + value: "data", + label: "Data", + icon: "dataset", + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + { + dataTestId: "navigateToMetadata", + value: "metadata", + label: "Metadata", + icon: "dataset_linked", + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + { + dataTestId: "navigateToHistory", + value: "history", + label: "History", + icon: "manage_history", + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + { + dataTestId: "navigateToLineage", + value: "lineage", + label: "Lineage", + icon: "account_tree", + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + + { + dataTestId: "navigateToDiscussion", + value: "discussions", + label: "Discussions", + icon: "message", + disabled: true, + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment) => { + return datasetPermissions.permissions.canView; + }, + }, + { + dataTestId: "navigateToFlows", + value: "flows", + label: "Flows", + icon: "add_task", + showAdminIcon: true, + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment, adminPrivileges?: boolean) => { + return Boolean(adminPrivileges) || datasetPermissions?.permissions.canSchedule; + }, + }, + { + dataTestId: "navigateToSettings", + value: "settings", + label: "Settings", + icon: "settings", + showAdminIcon: true, + shouldAllowTab: (datasetPermissions: DatasetPermissionsFragment, adminPrivileges?: boolean) => { + return ( + Boolean(adminPrivileges) || + datasetPermissions.permissions.canDelete || + datasetPermissions.permissions.canRename + ); + }, + }, +]; diff --git a/src/app/dataset-view/dataset.component.ts b/src/app/dataset-view/dataset.component.ts index b9d74d416..977b3d520 100644 --- a/src/app/dataset-view/dataset.component.ts +++ b/src/app/dataset-view/dataset.component.ts @@ -9,12 +9,12 @@ import { DatasetInfo } from "../interface/navigation.interface"; import { promiseWithCatch } from "../common/app.helpers"; import { DatasetRequestBySql } from "../interface/dataset.interface"; import { MaybeNull, MaybeUndefined } from "../common/app.types"; -import { DatasetPermissionsService } from "./dataset.permissions.service"; import { ReplaySubject, Subject, of } from "rxjs"; import { LineageGraphNodeData, LineageGraphNodeKind } from "./additional-components/lineage-component/lineage-model"; import _ from "lodash"; import { BaseDatasetDataComponent } from "../common/base-dataset-data.component"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { DatasetPermissionsService } from "./dataset.permissions.service"; @Component({ selector: "app-dataset", diff --git a/src/app/dataset-view/dataset.module.ts b/src/app/dataset-view/dataset.module.ts index 71fd9ece4..07be23bc7 100644 --- a/src/app/dataset-view/dataset.module.ts +++ b/src/app/dataset-view/dataset.module.ts @@ -86,6 +86,7 @@ import { FlowsComponent } from "./additional-components/flows-component/flows.co import { AngularMultiSelectModule } from "angular2-multiselect-dropdown"; import { MatTabsModule } from "@angular/material/tabs"; import { MatChipsModule } from "@angular/material/chips"; +import { DatasetViewMenuItemComponent } from "./dataset-view-menu/components/dataset-view-menu-item/dataset-view-menu-item.component"; @NgModule({ imports: [ @@ -165,6 +166,7 @@ import { MatChipsModule } from "@angular/material/chips"; FlowsTableComponent, AngularMultiSelectModule, MatChipsModule, + DatasetViewMenuItemComponent, ], declarations: [ DatasetViewHeaderComponent, @@ -208,6 +210,7 @@ import { MatChipsModule } from "@angular/material/chips"; RequestTimerComponent, AddDataModalComponent, FileFromUrlModalComponent, + DatasetViewMenuItemComponent, ], }) export class DatasetModule { diff --git a/src/app/services/local-storage.service.ts b/src/app/services/local-storage.service.ts index 7c4eb908c..0547eff1d 100644 --- a/src/app/services/local-storage.service.ts +++ b/src/app/services/local-storage.service.ts @@ -15,6 +15,17 @@ export class LocalStorageService { return localStorage.getItem(AppValues.LOCAL_STORAGE_LOGIN_REDIRECT_URL); } + public get adminPrivileges(): boolean | null { + const flag = localStorage.getItem(AppValues.LOCAL_STORAGE_ADMIN_PRIVILEGES); + if (flag) { + return Boolean(JSON.parse(flag)); + } else return null; + } + + public setAdminPriveleges(flag: boolean | null) { + localStorage.setItem(AppValues.LOCAL_STORAGE_ADMIN_PRIVILEGES, JSON.stringify(flag)); + } + public setRedirectAfterLoginUrl(url: string | null) { if (url) { localStorage.setItem(AppValues.LOCAL_STORAGE_LOGIN_REDIRECT_URL, url); diff --git a/src/assets/styles/components/_mat-toggle-group.scss b/src/assets/styles/components/_mat-toggle-group.scss index 8223085cd..101878239 100644 --- a/src/assets/styles/components/_mat-toggle-group.scss +++ b/src/assets/styles/components/_mat-toggle-group.scss @@ -53,6 +53,6 @@ mat-button-toggle { .mat-button-toggle-appearance-standard { .mat-button-toggle-label-content { - padding: 0 !important; + padding: 3px !important; } } diff --git a/src/styles.scss b/src/styles.scss index d2108317f..3f2c2a3ed 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1524,7 +1524,7 @@ input { color: black; div { - padding: 0 12px; + padding: 0 14px; } &:hover {