diff --git a/website/.eslintrc.json b/website/.eslintrc.json
index df0c92b..288302d 100644
--- a/website/.eslintrc.json
+++ b/website/.eslintrc.json
@@ -40,7 +40,6 @@
}
],
"@angular-eslint/no-output-native": "warn",
- "@angular-eslint/no-host-metadata-property": "warn",
// @typescript-eslint
"@typescript-eslint/explicit-member-accessibility": [
"error",
diff --git a/website/package.json b/website/package.json
index 4c5b112..1bbc427 100644
--- a/website/package.json
+++ b/website/package.json
@@ -14,20 +14,22 @@
},
"private": true,
"dependencies": {
- "@angular/common": "^19.2.0",
- "@angular/compiler": "^19.2.0",
- "@angular/core": "^19.2.0",
- "@angular/forms": "^19.2.0",
- "@angular/platform-browser": "^19.2.0",
- "@angular/platform-browser-dynamic": "^19.2.0",
- "@angular/platform-server": "^19.2.0",
- "@angular/router": "^19.2.0",
- "@angular/ssr": "^19.2.3",
+ "@angular/common": "19.2.5",
+ "@angular/compiler": "19.2.5",
+ "@angular/core": "19.2.5",
+ "@angular/forms": "19.2.5",
+ "@angular/platform-browser": "19.2.5",
+ "@angular/platform-browser-dynamic": "19.2.5",
+ "@angular/platform-server": "19.2.5",
+ "@angular/router": "19.2.5",
+ "@angular/ssr": "19.2.5",
"@fontsource/montserrat": "^5.2.5",
"@fontsource/roboto": "^5.2.5",
"@fortawesome/fontawesome-free": "^6.7.2",
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@popperjs/core": "^2.11.8",
+ "@typescript-eslint/eslint-plugin": "7",
+ "@typescript-eslint/parser": "7",
"bootstrap": "^5.3.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsdoc": "^50.6.8",
@@ -44,15 +46,19 @@
"@angular-eslint/schematics": "19.2.1",
"@angular-eslint/template-parser": "19.2.1",
"@angular/cli": "^19.2.3",
- "@angular/compiler-cli": "^19.2.0",
+ "@angular/compiler-cli": "19.2.5",
+ "@angular/localize": "19.2.5",
"@types/express": "^4.17.17",
"@types/jasmine": "~5.1.0",
"@types/node": "20",
- "@typescript-eslint/eslint-plugin": "^8.28.0",
- "@typescript-eslint/parser": "^8.28.0",
+ "@typescript-eslint/types": "^8.0.0",
+ "@typescript-eslint/utils": "^8.0.0",
"angular-eslint": "19.2.1",
- "eslint": "^9.23.0",
- "eslint-import-resolver-typescript": "^4.2.4",
+ "eslint": "^8.57.0",
+ "eslint-import-resolver-typescript": "^3.6.1",
+ "eslint-plugin-jasmine": "^4.2.2",
+ "eslint-plugin-local-rules": "^3.0.2",
+ "eslint-plugin-prefer-arrow": "1.2.2",
"jasmine-core": "~5.6.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@@ -60,7 +66,9 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.5.3",
- "typescript": "~5.7.2"
+ "typescript": "~5.7.2",
+ "typescript-eslint": "^8.0.0",
+ "vite": "^5.0.0"
},
"engines": {
"node": "^20.0"
diff --git a/website/src/app/app.component.spec.ts b/website/src/app/app.component.spec.ts
index 291333a..b8106a1 100644
--- a/website/src/app/app.component.spec.ts
+++ b/website/src/app/app.component.spec.ts
@@ -16,19 +16,5 @@ describe('AppComponent', () => {
expect(app)
.toBeTruthy();
});
-
- it('should have the \'meshstack-hub\' title', () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app.title)
- .toEqual('meshstack-hub');
- });
-
- it('should render title', () => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('h1')?.textContent)
- .toContain('Hello, meshstack-hub');
- });
});
+
diff --git a/website/src/app/app.component.ts b/website/src/app/app.component.ts
index 28ee2ed..6c54f9f 100644
--- a/website/src/app/app.component.ts
+++ b/website/src/app/app.component.ts
@@ -8,7 +8,7 @@ import { HeaderComponent } from 'app/shared/header/header.component';
import { FooterComponent } from './shared/footer';
@Component({
- selector: 'app-root',
+ selector: 'mst-root',
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, NgbModule],
templateUrl: './app.component.html',
standalone: true
diff --git a/website/src/app/app.config.ts b/website/src/app/app.config.ts
index 0d42c64..f606103 100644
--- a/website/src/app/app.config.ts
+++ b/website/src/app/app.config.ts
@@ -6,5 +6,10 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
- providers: [provideHttpClient(withFetch()), provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(withEventReplay())]
+ providers: [
+ provideHttpClient(withFetch()),
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ provideClientHydration(withEventReplay())
+ ]
};
diff --git a/website/src/app/core/template.ts b/website/src/app/core/template.ts
index 61c97e6..c580f6f 100644
--- a/website/src/app/core/template.ts
+++ b/website/src/app/core/template.ts
@@ -8,7 +8,7 @@ export interface Template {
githubUrls: {
ssh: string;
https: string;
- },
+ };
supportedPlatforms: PlatformType[];
}
diff --git a/website/src/app/features/template-details/import-dialog/import-dialog.component.html b/website/src/app/features/template-details/import-dialog/import-dialog.component.html
index 1ff9249..7929fd4 100644
--- a/website/src/app/features/template-details/import-dialog/import-dialog.component.html
+++ b/website/src/app/features/template-details/import-dialog/import-dialog.component.html
@@ -15,27 +15,40 @@
Provide link of your meshStack
to continue using this building block template
-
+
+
Don't use meshStack yet?
-
Check out and how it can help your platform team
-
+ Check it out and how it can help your platform team
+
Discover meshStack
diff --git a/website/src/app/features/template-details/import-dialog/import-dialog.component.ts b/website/src/app/features/template-details/import-dialog/import-dialog.component.ts
index 8266440..09e4cc6 100644
--- a/website/src/app/features/template-details/import-dialog/import-dialog.component.ts
+++ b/website/src/app/features/template-details/import-dialog/import-dialog.component.ts
@@ -1,34 +1,61 @@
-import { Component, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule, isPlatformBrowser } from '@angular/common';
+import { Component, Inject, Input, OnInit, PLATFORM_ID } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+interface ImportDialogForm {
+ meshStackUrl: FormControl
;
+}
+
@Component({
selector: 'mst-import-dialog',
- imports: [ReactiveFormsModule],
+ imports: [CommonModule, ReactiveFormsModule],
templateUrl: './import-dialog.component.html',
- styleUrl: './import-dialog.component.scss'
+ styleUrl: './import-dialog.component.scss',
+ standalone: true
})
export class ImportDialogComponent implements OnInit {
- public form!: FormGroup;
+ @Input()
+ public name!: string;
- public get meshStackUrl() {
- return this.form.get('meshStackUrl')?.value;
- }
+ @Input()
+ public modulePath!: string;
+
+ public form!: FormGroup;
constructor(
+ @Inject(PLATFORM_ID) private platformId: object,
public activeModal: NgbActiveModal,
private fb: FormBuilder
) { }
- ngOnInit(): void {
+ public ngOnInit(): void {
this.form = this.fb.group({
- meshStackUrl: ['']
+ meshStackUrl: this.fb.nonNullable.control('', [Validators.required, Validators.pattern(/^(https?:\/\/).*/)]),
});
}
- public open() {
-
+ public openMeshStackUrl() {
+ // Only runs in browser, not during SSR
+ if (isPlatformBrowser(this.platformId)) {
+ const url = this.getSanitizedMeshStackUrl() + '/#/building-block-definition-import?name=' + this.name + '&module-path=' + this.modulePath;
+ window.open(url.toString(), '_blank', 'noopener,noreferrer');
+ }
}
+ private getSanitizedMeshStackUrl(): string {
+ let meshStackUrl = this.form.controls.meshStackUrl.value;
+ const hashIndex = meshStackUrl.indexOf('#');
+
+ if (hashIndex !== -1) {
+ meshStackUrl = meshStackUrl.substring(0, hashIndex);
+ }
+
+ if (meshStackUrl.endsWith('/')) {
+ meshStackUrl = meshStackUrl.slice(0, -1);
+ }
+
+ return meshStackUrl;
+ }
}
diff --git a/website/src/app/features/template-details/template-details.component.html b/website/src/app/features/template-details/template-details.component.html
index 16b9a65..630d113 100644
--- a/website/src/app/features/template-details/template-details.component.html
+++ b/website/src/app/features/template-details/template-details.component.html
@@ -3,7 +3,7 @@
@@ -39,7 +39,7 @@
{{ template.source }}
-
diff --git a/website/src/app/features/template-details/template-details.component.scss b/website/src/app/features/template-details/template-details.component.scss
index 71168dd..e9c6da1 100644
--- a/website/src/app/features/template-details/template-details.component.scss
+++ b/website/src/app/features/template-details/template-details.component.scss
@@ -27,9 +27,9 @@
line-height: 2rem;
}
-.breadcrumb-item+.breadcrumb-item::before {
- content: "\f054";
- font-family: "Font Awesome 6 Free";
+.breadcrumb-item + .breadcrumb-item::before {
+ content: '\f054';
+ font-family: 'Font Awesome 6 Free';
font-weight: 900;
- color: gray
-}
\ No newline at end of file
+ color: gray;
+}
diff --git a/website/src/app/features/template-details/template-details.component.ts b/website/src/app/features/template-details/template-details.component.ts
index 9465569..e04f504 100644
--- a/website/src/app/features/template-details/template-details.component.ts
+++ b/website/src/app/features/template-details/template-details.component.ts
@@ -71,11 +71,25 @@ export class TemplateDetailsComponent implements OnInit, OnDestroy {
this.copyLabel = 'Copy';
}, 1000);
})
- .catch(e => console.log(e));
+ .catch(e =>
+ /* eslint-disable-next-line */
+ console.log(e)
+ );
}
- public open() {
- this.modalService.open(ImportDialogComponent, { size: 'lg', centered: true });
+ public open(template: TemplateDetailsVm) {
+ const regex = /modules\/[^/]+\/[^/]+/;
+ const match = template.source.match(regex);
+ const modulePath = match ? match[0] : '';
+
+ if (!modulePath) {
+ /* eslint-disable-next-line */
+ console.error('Module path not found in source URL');
+ } else {
+ const component = this.modalService.open(ImportDialogComponent, { size: 'lg', centered: true }).componentInstance;
+ component.name = template.name;
+ component.modulePath = modulePath;
+ }
}
}
diff --git a/website/src/app/features/template-gallery/template-gallery.component.ts b/website/src/app/features/template-gallery/template-gallery.component.ts
index 3017742..d734ff6 100644
--- a/website/src/app/features/template-gallery/template-gallery.component.ts
+++ b/website/src/app/features/template-gallery/template-gallery.component.ts
@@ -19,7 +19,6 @@ import { TemplateService } from 'app/shared/template';
standalone: true
})
export class TemplateGalleryComponent implements OnInit, OnDestroy {
-
public templates$!: Observable;
public searchForm!: FormGroup;
@@ -36,63 +35,62 @@ export class TemplateGalleryComponent implements OnInit, OnDestroy {
private fb: FormBuilder,
private templateService: TemplateService,
private platformLogoService: PlatformLogoService
- ) { }
+ ) {}
public ngOnInit(): void {
+ this.initializeSearchForm();
+ this.subscribeToRouteParams();
+ }
+
+ public ngOnDestroy(): void {
+ this.paramSubscription.unsubscribe();
+ }
+
+ public onSearch(): void {
+ const searchTerm = this.searchForm.value.searchTerm;
+ this.templates$ = this.getTemplatesWithLogos(
+ this.templateService.search(searchTerm)
+ );
+
+ this.isSearch = !!searchTerm;
+ this.router.navigate(['/all']);
+ }
+
+ private initializeSearchForm(): void {
this.searchForm = this.fb.group({
searchTerm: ['']
});
+ }
+ private subscribeToRouteParams(): void {
this.paramSubscription = this.route.paramMap.subscribe(params => {
const type = params.get('type') ?? 'all';
-
const templateObs$ = this.templateService.filterTemplatesByPlatformType(type as PlatformType);
- this.logos$ = this.platformLogoService.getLogoUrls();
- this.templates$ = forkJoin({
- templates: templateObs$,
- logos: this.logos$
- })
- .pipe(
- tap(() => this.isSearch = false),
- map(({ templates, logos }) => templates.map(item => ({
- cardLogo: item.logo,
- title: item.name,
- description: item.description,
- detailsRoute: `/template/${item.id}`,
- supportedPlatforms: item.supportedPlatforms.map(platform => ({ platformType: platform, imageUrl: logos[item.platformType] ?? null }))
- }))
- ),
- );
+ this.logos$ = this.platformLogoService.getLogoUrls();
+ this.templates$ = this.getTemplatesWithLogos(templateObs$);
});
}
- public ngOnDestroy(): void {
- this.paramSubscription.unsubscribe();
- }
-
- public onSearch(): void {
- const searchTerm = this.searchForm.value.searchTerm;
- this.templates$ = forkJoin({
- templates: this.templateService.search(searchTerm),
+ private getTemplatesWithLogos(templateObs$: Observable): Observable {
+ return forkJoin({
+ templates: templateObs$,
logos: this.logos$
})
.pipe(
- tap(() => {
- this.isSearch = !!searchTerm
- }),
- map(({ templates, logos }) => templates.map(item => ({
- cardLogo: item.logo,
- title: item.name,
- description: item.description,
- detailsRoute: `/template/${item.id}`,
- supportedPlatforms: item.supportedPlatforms.map(platform => ({ platformType: platform, imageUrl: logos[item.platformType] ?? null }))
- }))
+ tap(() => (this.isSearch = false)),
+ map(({ templates, logos }) =>
+ templates.map(item => ({
+ cardLogo: item.logo,
+ title: item.name,
+ description: item.description,
+ detailsRoute: `/template/${item.id}`,
+ supportedPlatforms: item.supportedPlatforms.map(platform => ({
+ platformType: platform,
+ imageUrl: logos[item.platformType] ?? null
+ }))
+ }))
)
);
-
- this.router.navigate(
- ['/all']
- );
}
}
diff --git a/website/src/app/shared/card/card.component.ts b/website/src/app/shared/card/card.component.ts
index 1cfa418..457da2c 100644
--- a/website/src/app/shared/card/card.component.ts
+++ b/website/src/app/shared/card/card.component.ts
@@ -13,12 +13,11 @@ import { Card } from './card';
})
export class CardComponent {
@Input()
- public card!:Card;
+ public card!: Card;
constructor(private router: Router) {}
public goToDetails() {
- console.log('Navigating to details route', this.card.detailsRoute);
this.router.navigate([this.card.detailsRoute]);
}
}
diff --git a/website/src/app/shared/template/template.service.ts b/website/src/app/shared/template/template.service.ts
index ec00fb6..046cee9 100644
--- a/website/src/app/shared/template/template.service.ts
+++ b/website/src/app/shared/template/template.service.ts
@@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
-import { Observable, map, shareReplay, take } from 'rxjs';
+import { Observable, map, take } from 'rxjs';
import { PlatformType, Template } from 'app/core';
diff --git a/website/src/index.html b/website/src/index.html
index c18249f..1def174 100644
--- a/website/src/index.html
+++ b/website/src/index.html
@@ -9,6 +9,6 @@
-
+