diff --git a/.gitignore b/.gitignore
index e4aede7..5258205 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
/frontend/node_modules/
/.stored_data/
/.env
+/.playwright-cli/
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/datacatalog.iml b/.idea/datacatalog.iml
deleted file mode 100644
index d0876a7..0000000
--- a/.idea/datacatalog.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index ab030fe..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 8fc95b6..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 4685f40..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/src/main/java/ebrainsv2/mip/datacatalog/configurations/SecurityConfiguration.java b/backend/src/main/java/ebrainsv2/mip/datacatalog/configurations/SecurityConfiguration.java
index 7dc7d5d..9e6f7a3 100644
--- a/backend/src/main/java/ebrainsv2/mip/datacatalog/configurations/SecurityConfiguration.java
+++ b/backend/src/main/java/ebrainsv2/mip/datacatalog/configurations/SecurityConfiguration.java
@@ -1,6 +1,5 @@
package ebrainsv2.mip.datacatalog.configurations;
-import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@@ -9,21 +8,36 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.stereotype.Component;
-
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@@ -38,6 +52,9 @@ public class SecurityConfiguration {
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;
+ @Value("${spring.security.oauth2.client.registration.keycloak.client-id:datacatalog}")
+ private String oidcClientId;
+
@Value("${authentication.enabled}")
private boolean authenticationEnabled;
@@ -63,11 +80,18 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
OAuth2AuthorizedClientService authorizedClientService) throws Exception {
if (authenticationEnabled) {
+ CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
+ csrfTokenRepository.setCookiePath("/");
+ csrfTokenRepository.setCookieName("MIP-XSRF-TOKEN");
+ csrfTokenRepository.setHeaderName("X-MIP-XSRF-TOKEN");
+
http
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // Allow access to any endpoint unless restricted by @PreAuthorize
)
.oauth2Login(oauth -> oauth
+ .userInfoEndpoint(userInfo -> userInfo.oidcUserService(oidcUserService()))
.defaultSuccessUrl(this.authCallbackUrl, true)
.successHandler((request, response, authentication) -> {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
@@ -88,7 +112,11 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
successHandler.setPostLogoutRedirectUri(this.frontendBaseUrl);
logout.logoutSuccessHandler(successHandler);
})
- .csrf(AbstractHttpConfigurer::disable); // Ensure CSRF protection as per requirements
+ .csrf(csrf -> csrf
+ .csrfTokenRepository(csrfTokenRepository)
+ .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
+ )
+ .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
} else {
http
.authorizeHttpRequests(auth -> auth
@@ -99,26 +127,106 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
return http.build();
}
- @Component
- @RequiredArgsConstructor
- static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper {
- private static Collection extractAuthorities(Map claims) {
- return ((Collection) claims.get("authorities")).stream()
- .map(SimpleGrantedAuthority::new)
- .collect(Collectors.toList());
- }
-
- @Override
- public Collection extends GrantedAuthority> mapAuthorities(Collection extends GrantedAuthority> authorities) {
+ @Bean
+ public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
+ return authorities -> {
Set mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
+ mappedAuthorities.add(authority);
+
if (authority instanceof OidcUserAuthority oidcUserAuthority) {
- mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims()));
+ mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims(), oidcClientId));
+ if (oidcUserAuthority.getUserInfo() != null) {
+ mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getUserInfo().getClaims(), oidcClientId));
+ }
}
});
return mappedAuthorities;
+ };
+ }
+
+ @Bean
+ public OAuth2UserService oidcUserService() {
+ OidcUserService delegate = new OidcUserService();
+ return (OidcUserRequest userRequest) -> {
+ OidcUser oidcUser = delegate.loadUser(userRequest);
+ Set mappedAuthorities = new LinkedHashSet<>();
+ mappedAuthorities.addAll(grantedAuthoritiesMapper().mapAuthorities(oidcUser.getAuthorities()));
+
+ String userNameAttributeName = userRequest.getClientRegistration()
+ .getProviderDetails()
+ .getUserInfoEndpoint()
+ .getUserNameAttributeName();
+
+ if (userNameAttributeName == null || userNameAttributeName.isBlank()) {
+ return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
+ }
+
+ return new DefaultOidcUser(
+ mappedAuthorities,
+ oidcUser.getIdToken(),
+ oidcUser.getUserInfo(),
+ userNameAttributeName
+ );
+ };
+ }
+
+ private static Collection extractAuthorities(Map claims, String clientId) {
+ Set authorities = new LinkedHashSet<>();
+
+ authorities.addAll(asStringCollection(claims.get("authorities")));
+ authorities.addAll(asStringCollection(claims.get("roles")));
+
+ Object realmAccess = claims.get("realm_access");
+ if (realmAccess instanceof Map, ?> realmAccessMap) {
+ authorities.addAll(asStringCollection(realmAccessMap.get("roles")));
+ }
+
+ Object resourceAccess = claims.get("resource_access");
+ if (resourceAccess instanceof Map, ?> resourceAccessMap) {
+ Object clientAccess = resourceAccessMap.get(clientId);
+ if (clientAccess instanceof Map, ?> clientAccessMap) {
+ authorities.addAll(asStringCollection(clientAccessMap.get("roles")));
+ }
+ }
+
+ return authorities.stream()
+ .filter(Objects::nonNull)
+ .filter(role -> !role.isBlank())
+ .flatMap(role -> normalizeAuthorities(role).stream())
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ }
+
+ private static Collection normalizeAuthorities(String role) {
+ String normalizedRole = role.trim();
+ if (normalizedRole.startsWith("ROLE_")) {
+ return List.of(normalizedRole, normalizedRole.substring("ROLE_".length()));
+ }
+ return List.of(normalizedRole, "ROLE_" + normalizedRole);
+ }
+
+ private static Collection asStringCollection(Object value) {
+ if (value instanceof Collection> collection) {
+ return collection.stream()
+ .filter(Objects::nonNull)
+ .map(String::valueOf)
+ .toList();
+ }
+ return List.of();
+ }
+
+ static class CsrfCookieFilter extends OncePerRequestFilter {
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
+ if (csrfToken != null) {
+ csrfToken.getToken();
+ }
+ filterChain.doFilter(request, response);
}
}
}
diff --git a/frontend/angular.json b/frontend/angular.json
index 7d43e81..ffa12a7 100644
--- a/frontend/angular.json
+++ b/frontend/angular.json
@@ -42,8 +42,8 @@
},
{
"type": "anyComponentStyle",
- "maximumWarning": "2kB",
- "maximumError": "4kB"
+ "maximumWarning": "4kB",
+ "maximumError": "6kB"
}
],
"outputHashing": "all",
diff --git a/frontend/src/DESIGN_SYSTEM.yaml b/frontend/src/DESIGN_SYSTEM.yaml
new file mode 100644
index 0000000..534cbb4
--- /dev/null
+++ b/frontend/src/DESIGN_SYSTEM.yaml
@@ -0,0 +1,115 @@
+agent_name: MIP_BrandGuard
+purpose: >
+ Enforce MIP (Medical Informatics Platform) brand guidelines when generating or reviewing
+ marketing, web, UI, print, and social assets. Ensure correct logo usage, colors, and typography.
+
+source_document:
+ title: "MIP Charte graphique"
+ date: "2023-10-02"
+ creator: "Donovan Studio"
+ client: "CHUV"
+
+brand_identity:
+ brand_name: "MIP"
+ baseline: "Medical Informatics Platform"
+
+logo_system:
+ official_variants:
+ - id: symbol_only
+ description: "Symbol without wordmark"
+ usage: ["social avatar", "url/favicon/icon"]
+ minimum_size: { px: 30, mm: 10 }
+ - id: primary
+ description: "Symbol + 'mip' wordmark"
+ usage: ["website", "email", "stationery", "poster"]
+ minimum_size: { px: 50 }
+ - id: secondary_with_baseline
+ description: "Logo with baseline 'Medical Informatics Platform'"
+ usage: ["formal comms", "stationery/poster", "website/email", "signage"]
+ minimum_size: { px: 90 }
+
+ protection_zone:
+ rule: >
+ Always keep clear space around the logo (zone de protection) as defined in the charter.
+ No text, images, borders, or UI elements may enter this exclusion area.
+ enforcement: "Reject placements that violate clear space."
+
+ construction_rules:
+ rule: >
+ Do not redraw, stretch, skew, rotate, outline, add effects, or modify proportions of any logo variant.
+ Do not rearrange symbol, wordmark, or baseline.
+ enforcement: "If a requested transformation changes geometry, refuse and propose compliant alternative."
+
+color_palette:
+ primary_intent: "Digital-first palette for web, social, video, digital interfaces."
+ print_guidance: "For physical/print supports, prefer black logo on white."
+ colors:
+ - name: dark_blue
+ hex: "#2B33E9"
+ rgb: [43, 51, 233]
+ cmyk_approx: [89.85, 74.4, 0, 0]
+ - name: light_blue
+ hex: "#7F9CE8"
+ rgb: [127, 156, 232]
+ cmyk_approx: [54.62, 35.3, 0, 0]
+ - name: orange
+ hex: "#FFBA08"
+ rgb: [255, 186, 8]
+ cmyk_approx: [0, 30.98, 92.37, 0]
+ - name: light_green
+ hex: "#DFEFE4"
+ rgb: [223, 239, 228]
+ cmyk_approx: [15.88, 0, 13.86, 0]
+
+approved_logo_color_combinations:
+ - id: white_on_dark_blue
+ - id: blue_on_light_blue
+ - id: black_on_white
+ - id: blue_on_orange
+ - id: blue_on_green
+ - id: white_on_black
+rule: "Only these logo/background combinations are permitted. Any other colorway must be rejected."
+
+typography:
+ logo_font:
+ name: "Mark Pro Bold"
+ foundry: "FontFont"
+ usage: "Logo only (do not substitute)."
+ brand_text_font:
+ name: "Alaska Regular"
+ foundry: "Newglyph"
+ usage: "All identity text compositions for HIP and MIP."
+rule: "Use Alaska Regular for body/headings unless explicitly constrained; never replace logo type."
+
+compliance_checks:
+ - check: logo_variant_selected_correctly
+ pass_if: "Variant matches intended medium (icon/social vs web/print vs formal baseline)."
+ - check: minimum_size_respected
+ pass_if: "symbol_only>=30px/10mm; primary>=50px; secondary>=90px."
+ - check: clear_space_respected
+ pass_if: "No elements intrude into protection zone."
+ - check: approved_colors_only
+ pass_if: "Palette hex values used; no arbitrary near-matches."
+ - check: logo_colorway_approved
+ pass_if: "Logo/background matches one of the approved combinations."
+ - check: print_recommendation
+ pass_if: "If print and no strong reason otherwise: black on white preferred."
+ - check: typography_correct
+ pass_if: "Alaska Regular used for text; Mark Pro Bold only for logo."
+
+default_decision_policy:
+ when_uncertain: >
+ Choose the most conservative compliant option:
+ - Primary logo for general web/email
+ - Symbol-only for avatars/icons
+ - Secondary with baseline for formal material
+ - Black-on-white for print
+ refusal_style: >
+ If user request conflicts with guidelines, respond with:
+ (1) why it conflicts,
+ (2) the closest compliant alternative,
+ (3) what info is needed to choose between compliant options (only if essential).
+
+output_expectations:
+ - "When generating design specs, always list: logo variant, size, clear-space, colors (hex), typography."
+ - "When reviewing assets, return a pass/fail plus concrete fixes."
diff --git a/frontend/src/app/app.component.css b/frontend/src/app/app.component.css
index 61c3ef1..c44f8c1 100644
--- a/frontend/src/app/app.component.css
+++ b/frontend/src/app/app.component.css
@@ -1,34 +1,5 @@
-/* app.component.css */
-.main-header {
- background-color: #007BFF;
- padding: 20px;
- text-align: center;
- color: white;
-}
-
-.nav-bar {
- display: flex;
- justify-content: center;
- gap: 20px;
-}
-
-.nav-link {
- color: white;
- text-decoration: none;
- font-size: 1.2em;
-}
-
-.nav-link:hover {
- text-decoration: underline;
-}
-
main {
padding: 20px;
- min-height: 80vh; /* Ensures the content area takes up space */
-}
-
-.main-footer {
- background-color: #f1f1f1;
- padding: 10px;
- text-align: center;
+ min-height: 80vh;
+ padding-top: 96px;
}
diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html
index 6246a8d..401b09e 100644
--- a/frontend/src/app/app.component.html
+++ b/frontend/src/app/app.component.html
@@ -1,6 +1,5 @@
-
diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts
index fedc3ef..8bae25f 100644
--- a/frontend/src/app/app.component.ts
+++ b/frontend/src/app/app.component.ts
@@ -1,5 +1,6 @@
+import { ViewportScroller } from '@angular/common';
import { FooterComponent } from './pages/shared/footer/footer.component';
-import { Component, OnInit } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { HeaderComponent } from './pages/shared/header/header.component';
import { RouterModule } from '@angular/router';
@@ -10,19 +11,11 @@ import { RouterModule } from '@angular/router';
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
-export class AppComponent implements OnInit {
+export class AppComponent {
+ private readonly viewportScroller = inject(ViewportScroller);
title = 'datacatalog-frontend';
- ngOnInit(): void {
- this.clearTokenOnInit();
- }
-
- private clearTokenOnInit(): void {
- // Check if the token should be cleared (example condition)
- const token = localStorage.getItem('auth_token');
- if (token) {
- localStorage.removeItem('auth_token');
- console.log('Token cleared during initialization');
- }
+ constructor() {
+ this.viewportScroller.setOffset(() => [0, 80]);
}
}
diff --git a/frontend/src/app/app.routes.spec.ts b/frontend/src/app/app.routes.spec.ts
new file mode 100644
index 0000000..11bf19e
--- /dev/null
+++ b/frontend/src/app/app.routes.spec.ts
@@ -0,0 +1,17 @@
+import { LandingPageComponent } from './pages/landing-page/landing-page.component';
+import { HomeSectionRedirectComponent } from './pages/home-section-redirect/home-section-redirect.component';
+import { appRoutes } from './app.routes';
+
+describe('appRoutes', () => {
+ it('uses the root path for the landing page', () => {
+ const homeRoute = appRoutes.find((route) => route.path === '');
+
+ expect(homeRoute?.component).toBe(LandingPageComponent);
+ });
+
+ it('keeps /home as a legacy alias that redirects back to the landing page', () => {
+ const legacyHomeRoute = appRoutes.find((route) => route.path === 'home');
+
+ expect(legacyHomeRoute?.component).toBe(HomeSectionRedirectComponent);
+ });
+});
diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts
index 0fc6a83..d7271db 100644
--- a/frontend/src/app/app.routes.ts
+++ b/frontend/src/app/app.routes.ts
@@ -1,35 +1,48 @@
-import {RouterModule, Routes} from '@angular/router';
+import { RouterModule, Routes } from '@angular/router';
import { LandingPageComponent } from './pages/landing-page/landing-page.component';
-import { FederationsPageComponent } from './pages/federations-page/federations-page.component';
import { AccountPageComponent } from './pages/account-page/account-page.component';
import { AuthGuard } from './guards/auth.guard';
-import {NgModule} from "@angular/core";
-import {AuthCallbackComponent} from "./callback/authcallback.component";
-import {DataModelsPageComponent} from "./pages/data-models-page/data-models-page.component";
-import {AboutPageComponent} from "./pages/about-page/about-page.component";
+import { NgModule } from '@angular/core';
+import { AuthCallbackComponent } from './callback/authcallback.component';
+import { PathologyPageComponent } from './pages/pathology-page/pathology-page.component';
+import { HomeSectionRedirectComponent } from './pages/home-section-redirect/home-section-redirect.component';
export const appRoutes: Routes = [
- { path: 'home', component: LandingPageComponent },
+ { path: '', component: LandingPageComponent },
{
- path: 'federations',
- component: FederationsPageComponent, // This is fine for the parent page
+ path: 'home',
+ component: HomeSectionRedirectComponent,
+ },
+ {
+ path: 'pathology',
+ component: PathologyPageComponent,
loadChildren: () =>
- import('./pages/federations-page/federations-page.module').then(
- (m) => m.FederationsPageModule // Ensure you're importing the main module here
+ import('./pages/pathology-page/pathology-page.module').then(
+ (m) => m.PathologyPageModule
),
},
- { path: 'account', component: AccountPageComponent, canActivate: [AuthGuard] },
- { path: 'data-models', component: DataModelsPageComponent,
+ {
+ path: 'federations',
+ pathMatch: 'full',
+ component: HomeSectionRedirectComponent,
+ data: { fragment: 'federations' },
+ },
+ {
+ path: 'federations',
loadChildren: () =>
- import('./pages/data-models-page/data-models-page.module').then(
- (m) => m.DataModelsPageModule // Ensure you're importing the main module here
+ import('./pages/federations-page/federations-page.module').then(
+ (m) => m.FederationsPageModule
),
},
+ { path: 'account', component: AccountPageComponent, canActivate: [AuthGuard] },
{ path: 'auth-callback', component: AuthCallbackComponent },
- { path: 'about', component: AboutPageComponent },
- { path: '', redirectTo: 'home', pathMatch: 'full' },
- { path: '**', redirectTo: 'home' }
+ {
+ path: 'about',
+ component: HomeSectionRedirectComponent,
+ data: { fragment: 'about' },
+ },
+ { path: '**', redirectTo: '' }
];
@NgModule({
diff --git a/frontend/src/app/interfaces/federations.interface.ts b/frontend/src/app/interfaces/federations.interface.ts
index 1c90841..923d3b4 100644
--- a/frontend/src/app/interfaces/federations.interface.ts
+++ b/frontend/src/app/interfaces/federations.interface.ts
@@ -1,4 +1,4 @@
-import {DataModel} from "./data-model.interface";
+import {Pathology} from "./pathology.interface";
export interface Federation {
code: string;
@@ -6,7 +6,7 @@ export interface Federation {
url: string;
description: string;
dataModelIds: string[];
- dataModels: DataModel[];
+ pathologies: Pathology[];
institutions: string;
records: string;
}
diff --git a/frontend/src/app/interfaces/data-model.interface.ts b/frontend/src/app/interfaces/pathology.interface.ts
similarity index 84%
rename from frontend/src/app/interfaces/data-model.interface.ts
rename to frontend/src/app/interfaces/pathology.interface.ts
index 5863df0..d2816c3 100644
--- a/frontend/src/app/interfaces/data-model.interface.ts
+++ b/frontend/src/app/interfaces/pathology.interface.ts
@@ -1,4 +1,4 @@
-// Interface for a Variable in the data model
+// Interface for a Variable in the pathology tree
export interface Variable {
label: string;
code?: string;
@@ -13,7 +13,7 @@ export interface Variable {
maxValue?: number;
}
-// Interface for a Group in the data model
+// Interface for a Group in the pathology tree
export interface Group {
label: string;
code?: string;
@@ -21,8 +21,8 @@ export interface Group {
groups?: Group[];
}
-// Interface for the overall DataModel
-export interface DataModel {
+// Interface for the overall Pathology
+export interface Pathology {
uuid: string;
label: string;
code?: string;
@@ -30,7 +30,7 @@ export interface DataModel {
longitudinal?: boolean; // Root level only
variables?: Variable[];
groups?: Group[];
- released:boolean;
+ released: boolean;
}
// D3 hierarchy format interface
diff --git a/frontend/src/app/pages/about-page/about-page.component.css b/frontend/src/app/pages/about-page/about-page.component.css
index b20c992..a2ca823 100644
--- a/frontend/src/app/pages/about-page/about-page.component.css
+++ b/frontend/src/app/pages/about-page/about-page.component.css
@@ -1,80 +1,101 @@
-/* Host padding ensures content sits below fixed header */
:host {
display: block;
- padding-top: 80px;
}
-/* Container Styling */
-.about-container {
- max-width: 900px;
- margin: 40px auto;
- padding: 20px;
- background: #ffffff;
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
- border-radius: 10px;
- font-family: 'Roboto', sans-serif;
- color: #333;
- line-height: 1.6;
+.about-page {
+ width: 100%;
}
-/* Header Styling */
-.header {
- text-align: center;
- margin-bottom: 30px;
+.hero {
+ display: grid;
+ grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
+ gap: 28px;
+ align-items: start;
+ margin-bottom: 36px;
+}
+
+
+
+.hero-card,
+.about-card {
+ border: 1px solid rgba(255, 255, 255, 0.4);
+ border-radius: 28px;
+ background: rgba(255, 255, 255, 0.65);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ box-shadow: 0 16px 32px rgba(18, 32, 63, 0.03);
+ transition: all 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-card:hover,
+.about-card:hover {
+ transform: translateY(-8px) scale(1.01);
+ box-shadow: 0 32px 64px rgba(18, 32, 63, 0.08);
+ border-color: rgba(255, 255, 255, 0.9);
+}
+
+.hero-card {
+ padding: 24px;
}
.logo {
- width: 80px;
- height: 80px;
- margin-bottom: 15px;
+ width: 154px;
+ height: auto;
+ margin-bottom: 18px;
}
-.title {
- font-size: 2.4em;
- color: #333;
- font-weight: 500;
+.hero-card p {
+ margin: 0;
+ color: rgba(18, 32, 63, 0.78);
+ line-height: 1.7;
}
-/* About Content Styling */
-.about-content {
- padding: 20px;
- text-align: left;
+.about-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 24px;
}
-.about-content h2 {
- font-size: 1.8em;
- margin-bottom: 20px;
- color: #555;
+.about-card {
+ padding: 24px 26px;
}
-.about-content p {
- font-size: 1.1em;
- margin-bottom: 15px;
- color: #555;
+.about-card h2 {
+ margin: 0 0 14px;
+ color: var(--mip-ink);
+ font-family: var(--mip-font-brand);
+ font-size: 1.65rem;
}
-.about-content a {
- color: #007bff;
- font-weight: 500;
- text-decoration: none;
+.about-card p {
+ margin: 0 0 14px;
+ color: rgba(18, 32, 63, 0.76);
+ line-height: 1.75;
}
-.about-content a:hover {
- text-decoration: underline;
- color: #0056b3;
+.about-card p:last-child {
+ margin-bottom: 0;
}
-/* Responsive Design */
-@media (max-width: 768px) {
- .about-container {
- padding: 15px;
- }
+.about-card a {
+ color: var(--mip-dark-blue);
+ font-weight: 700;
+ text-decoration: none;
+}
- .title {
- font-size: 2em;
- }
+.about-card-wide {
+ grid-column: 1 / -1;
+}
- .about-content p {
- font-size: 1em;
+@media (max-width: 900px) {
+ .hero,
+ .about-grid {
+ grid-template-columns: 1fr;
}
}
+
+@media (max-width: 768px) {
+ /* Inherited from parent container when embedded */
+}
diff --git a/frontend/src/app/pages/about-page/about-page.component.html b/frontend/src/app/pages/about-page/about-page.component.html
index c5ada5e..9e67fe0 100644
--- a/frontend/src/app/pages/about-page/about-page.component.html
+++ b/frontend/src/app/pages/about-page/about-page.component.html
@@ -1,34 +1,62 @@
-
-
-
-
-
The Medical Informatics Platform
+
+ @if (!isEmbedded) {
+
+
+
Project documentation
+
The Medical Informatics Platform
+
+ A federated environment for decentralized clinical data analysis fits large multi-site
+ studies.
+
+
+
+
+
+
+ MIP connects institutions, pathology catalogs, and research tooling through a privacy-first
+ infrastructure that fits large multi-site studies.
+
+
+}
+
+
+
+
What it does
+
+ The Medical Informatics Platform (MIP) is a fully operational open-source platform for
+ sharing decentralized clinical data.
+
+
+ Clinical data that cannot be shared, transferred, or stored in a centralized way can still
+ be federated and collaboratively analyzed.
+
+
+
+
+
Why it matters
+
+ Data owners keep full control of access and sharing through controlled accreditation,
+ access control, and user management.
+
+
+ This makes large-scale, privacy-preserving research practical across institutions and
+ disease domains.
+
+
-
-
-
About
-
- The Medical Informatics Platform (MIP) is the most advanced, fully
- operational, open-source platform for sharing decentralized clinical
- data.
-
-
- Clinical data that cannot be shared, transferred, and stored in a
- centralized way can be federated and collaboratively analyzed.
-
-
- Data Owners have full control of accessibility and sharing of their data
- through a tightly controlled accreditation, access control, and user
- management system.
-
-
- Documentation about the project can be found on
- GitHub.
-
This guide is optimized for larger screens. Please use a desktop or tablet for the best experience.
-
Welcome! Here's a quick guide to using this data model visualizer:
-
-
- Yellow-highlighted nodes can be
- double-clicked to set them as the new root.
- Use the breadcrumb at the top-left of the chart to view the current root or navigate back to previous nodes.
-
-
- Enable or disable the chart's zoom functionality as needed:
-
-
Zoom is ideal for shallow or less deeply nested trees.
-
Disabling zoom is better for more complex, deeply nested trees.
-
-
-
- Search for specific variables or groups using the search icon. You can also filter variables by type.
-
-
- Selecting a group sets it as the new root.
- Selecting a variable sets its parent group as the root.
-
-
- Adjust the tree depth by selecting a value from the dropdown.
-
-
- Use the dropdowns above to select federations and data models.
-
-
- Download data by clicking the "Download" button on the right.
-
+ Access decentralized real-world data across medical centers.
+ Perform secure analysis while preserving patient privacy using
+ our federated architecture.
- Collaborate across multiple hospitals to access medical data and use
- cutting-edge algorithms to analyze shared pathologies like dementia,
- mental health, and more.
-
-
- Make sure you have an EBRAINS Account to access the platform.
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ Scroll to explore
+
+
+
+
+
-
-
+
-
-
-
-
+
+
+
-
-
Access to Medical Data
-
- Explore tools that enable secure access to medical data from our federated network of hospitals. Gain insights while maintaining privacy and compliance.
-
-
-
-
+
-
-
-
Algorithms
-
- Leverage cutting-edge algorithms for analyzing medical data, including advanced tools for diagnosing pathologies like dementia or mental health conditions.
-