Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ cypress/downloads/

# Cypress cache
.cypress_cache/
/projects/dashboard-core/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2026 Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Fraunhofer-Gesellschaft zur Förderung der angewandten Forschung e.V. - initial API and implementation
*
*/

import { Component, Type } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { mount } from 'cypress/angular';
import { AppConfig, DashboardAppComponent } from '@eclipse-edc/dashboard-core';

@Component({
selector: 'lib-navbar-start-stub',
standalone: true,
template: '<span data-cy="start-widget">start-widget</span>',
})
class NavbarStartStubComponent {}

@Component({
selector: 'lib-navbar-center-stub',
standalone: true,
template: '<span data-cy="center-widget">center-widget</span>',
})
class NavbarCenterStubComponent {}

@Component({
selector: 'lib-navbar-end-stub',
standalone: true,
template: '<span data-cy="end-widget">end-widget</span>',
})
class NavbarEndStubComponent {}

const appConfig: AppConfig = {
appTitle: 'Test Dashboard',
enableUserConfig: false,
menuItems: [
{ text: 'Home', materialSymbol: 'home', routerPath: 'home' },
{ text: 'Assets', materialSymbol: 'deployed_code_update', routerPath: 'assets' },
],
};

const mountDashboard = (
navbarStartComponents: Type<unknown>[],
navbarCenterComponents: Type<unknown>[],
navbarEndComponents: Type<unknown>[],
) =>
mount(DashboardAppComponent, {
autoDetectChanges: true,
componentProperties: {
appConfig: Promise.resolve(appConfig),
themes: ['light'],
navbarStartComponents,
navbarCenterComponents,
navbarEndComponents,
},
providers: [provideRouter([]), provideHttpClient()],
});

describe('DashboardAppComponent navbar extension', () => {
it('renders injected components in the navbar-start region', () => {
mountDashboard([NavbarStartStubComponent], [], []);

cy.get('.navbar-start [data-cy="start-widget"]').should('exist');
});

it('renders injected components in the navbar-center region', () => {
mountDashboard([], [NavbarCenterStubComponent], []);

cy.get('.navbar-center [data-cy="center-widget"]').should('exist');
});

it('renders injected components in the navbar-end region', () => {
mountDashboard([], [], [NavbarEndStubComponent]);

cy.get('.navbar-end [data-cy="end-widget"]').should('exist');
});

it('preserves the built-in navbar content alongside injected components', () => {
mountDashboard([NavbarStartStubComponent], [NavbarCenterStubComponent], [NavbarEndStubComponent]);

// Built-in menu toggle button remains in the start region.
cy.get('.navbar-start button').should('exist');
// Built-in app title remains in the center region.
cy.get('.navbar-center').contains('Test Dashboard').should('exist');
// Built-in theme switcher dropdown remains in the end region.
cy.get('.navbar-end .dropdown-end').should('exist');
});

it('appends injected start components after the built-in content', () => {
mountDashboard([NavbarStartStubComponent], [], []);

cy.get('.navbar-start').find('lib-navbar-start-stub').prevAll('button').should('exist');
});

it('appends injected center components after the built-in content', () => {
mountDashboard([], [NavbarCenterStubComponent], []);

cy.get('.navbar-center').find('lib-navbar-center-stub').prevAll().contains('Test Dashboard').should('exist');
});

it('renders no injected components by default', () => {
mountDashboard([], [], []);

cy.get('[data-cy="start-widget"]').should('not.exist');
cy.get('[data-cy="center-widget"]').should('not.exist');
cy.get('[data-cy="end-widget"]').should('not.exist');
// Navbar itself still renders.
cy.get('.navbar').should('exist');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@
class="btn btn-lg btn-circle btn-ghost hover:text-primary-content hover:bg-primary border-none"
role="button"
(click)="stateService.toggleMenuOpen()"
[disabled]="((appConfig | async)?.menuItems)!.length < 2"
[disabled]="((appConfig | async)?.menuItems?.length ?? 0) < 2"
>
@if (stateService.isMenuOpen$ | async) {
<span class="material-symbols-rounded h7 w-7 !text-[1.75rem]">menu_open</span>
} @else {
<span class="material-symbols-rounded h7 w-7 !text-[1.75rem]">menu</span>
}
</button>
@for (component of navbarStartComponents; track component) {
<ng-container *ngComponentOutlet="component"></ng-container>
}
</div>
<div class="navbar-center">
<div class="text-lg">{{ (appConfig | async)?.appTitle ?? 'EDC Dashboard' }}</div>
Expand Down Expand Up @@ -139,6 +142,9 @@
</button>
</div>
}
@for (component of navbarCenterComponents; track component) {
<ng-container *ngComponentOutlet="component"></ng-container>
}
</div>
<div class="navbar-end">
<div class="dropdown dropdown-end">
Expand Down Expand Up @@ -171,11 +177,14 @@
}
</ul>
</div>
@for (component of navbarEndComponents; track component) {
<ng-container *ngComponentOutlet="component"></ng-container>
}
</div>
</div>

<div class="box-border py-4 pl-4 flex flex-auto overflow-auto">
@if (((appConfig | async)?.menuItems)!.length > 1) {
@if (((appConfig | async)?.menuItems?.length ?? 0) > 1) {
<ul
class="menu flex-nowrap shrink-0 h-full rounded-2xl bg-neutral text-neutral-content shadow-xl [&_li>a]:px-4 overflow-auto"
>
Expand All @@ -202,7 +211,7 @@
<div
id="router-content"
class="px-4 box-border w-full"
[ngClass]="((appConfig | async)?.menuItems)!.length > 1 ? '' : 'pl-0'"
[ngClass]="((appConfig | async)?.menuItems?.length ?? 0) > 1 ? '' : 'pl-0'"
>
<router-outlet></router-outlet>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
*
*/

import { AfterViewInit, Component, Input, ViewChild, ViewContainerRef, inject } from '@angular/core';
import { AfterViewInit, Component, Input, Type, ViewChild, ViewContainerRef, inject } from '@angular/core';
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
import { AsyncPipe, NgClass } from '@angular/common';
import { AsyncPipe, NgClass, NgComponentOutlet } from '@angular/common';
import { DashboardStateService } from '../services/dashboard-state.service';
import { EdcConfig } from '../models/edc-config';
import { EdcClientService } from '../services/edc-client.service';
Expand All @@ -26,7 +26,7 @@ import { AppConfig } from '../models/app-config';
@Component({
selector: 'lib-dashboard-app',
standalone: true,
imports: [RouterOutlet, RouterLink, RouterLinkActive, AsyncPipe, NgClass],
imports: [RouterOutlet, RouterLink, RouterLinkActive, AsyncPipe, NgClass, NgComponentOutlet],
templateUrl: './dashboard-app.component.html',
styleUrl: './dashboard-app.component.css',
})
Expand All @@ -39,6 +39,13 @@ export class DashboardAppComponent implements AfterViewInit {
@Input() edcConfigs?: Promise<EdcConfig[]>;
@Input() themes: string[] = [];

/** Components appended to the navbar-start region, rendered after the built-in content. */
@Input() navbarStartComponents: Type<unknown>[] = [];
/** Components appended to the navbar-center region, rendered after the built-in content. */
@Input() navbarCenterComponents: Type<unknown>[] = [];
/** Components appended to the navbar-end region, rendered after the built-in content. */
@Input() navbarEndComponents: Type<unknown>[] = [];

@ViewChild('dashboardModal', { read: ViewContainerRef, static: true }) modal!: ViewContainerRef;
@ViewChild('dashboardAlert', { read: ViewContainerRef, static: true }) alert!: ViewContainerRef;

Expand Down
Loading