Skip to content

Commit 28388bb

Browse files
KasinhouMatus Kasakclaude
authored
JCU/Add deployed-version info: VERSION_D at /static/VERSION_D (#813) (#1478)
* Add deployed-version info feature: serve VERSION_D at /static/VERSION_D (#813) Implement the deployed-commit info feature for this DSpace 9 / Angular 20 (standalone) instance, mirroring dtq-dev's behaviour: - scripts/sourceversion.py generates src/static-files/VERSION_D.html (git hash, commit date, build-run link) at Docker build time. - .github/workflows/docker.yml: enable run_python_version_script (both image jobs) so the version file is produced by the reusable build. - New standalone static-page feature (component + routes + HtmlContentService + dsSafeHtml pipe) renders /static/<name> from static-files/<name>.html; content is fetched client-side (SSR-safe). VERSION_D is reachable at /static/VERSION_D. - angular.json registers src/static-files as a build asset. - app-routes.ts registers the /static route. - en/cs i18n: static-page.404.* strings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Add src/static-files placeholder so VERSION_D is generated & served (#813) TUL/jcu had no src/static-files directory. The CI 'Add version' step redirects into src/static-files/VERSION_D.html and angular.json ships src/static-files as a build asset, both of which require the directory to exist. Commit a placeholder (overwritten at build time) to guarantee it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Scope static-files asset to VERSION_D.html; fix eslint (import sort, no alias) (#813) - angular.json: restrict the static-files asset to VERSION_D.html so only /static/VERSION_D is served (not a full static-page set). - html-content.service.ts: drop the 'of as observableOf' alias (alias-imports rule). - app-routes.ts / static-page.component.ts: sort imports (simple-import-sort, sort-standalone-imports). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Trim comments to match dtq-dev; drop issue refs and redundant notes Remove issue-tracker references and extra explanatory comments from the frontend deployed-version files, aligning comment style with dtq-dev, and drop the redundant explicit 'standalone: true' on the pipe (default in Angular 20). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 97a5c89 commit 28388bb

14 files changed

Lines changed: 309 additions & 4 deletions

.github/workflows/docker.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ jobs:
3434
tags_flavor: suffix=-dev
3535
# As this is a "dev" image, its tags are all suffixed with "-dev". Otherwise, it uses the same
3636
# tagging logic as the primary 'dspace/dspace-angular' image above.
37-
# run_python_version_script: true
38-
# python_version_script_dest: src/static-files/VERSION_D.html
37+
run_python_version_script: true
38+
python_version_script_dest: src/static-files/VERSION_D.html
3939
secrets:
4040
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
4141
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
@@ -52,8 +52,8 @@ jobs:
5252
build_id: dspace-angular
5353
image_name: dataquest/dspace-angular
5454
dockerfile_path: ./Dockerfile.dist
55-
# run_python_version_script: true
56-
# python_version_script_dest: src/static-files/VERSION_D.html
55+
run_python_version_script: true
56+
python_version_script_dest: src/static-files/VERSION_D.html
5757
secrets:
5858
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
5959
DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}

angular.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
"aot": true,
4141
"assets": [
4242
"src/assets",
43+
{
44+
"glob": "VERSION_D.html",
45+
"input": "src/static-files",
46+
"output": "static-files"
47+
},
4348
"src/robots.txt"
4449
],
4550
"styles": [

scripts/sourceversion.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import subprocess
2+
import sys
3+
from datetime import datetime, timezone
4+
5+
# when next editing this script, please introduce argparse.
6+
# do not forget, it is called in BE by .github\workflows\reusable-docker-build.yml
7+
# argparse must be introduced there.
8+
# that action also calls BE version of this script, which is different (BE: scripts/sourceversion.py).
9+
# It must also cooperate with argparse
10+
11+
# the idea is, that this will be different on each branch, but could be possibly passed by argv/argparse
12+
RELEASE_TAG_BASE='none'
13+
14+
def get_time_in_timezone(zone: str = "Europe/Bratislava"):
15+
try:
16+
from zoneinfo import ZoneInfo
17+
my_tz = ZoneInfo(zone)
18+
except Exception as e:
19+
my_tz = timezone.utc
20+
return datetime.now(my_tz)
21+
22+
23+
if __name__ == '__main__':
24+
ts = get_time_in_timezone()
25+
# we have html tags, since this script ends up creating VERSION_D.html
26+
print(f"<h4>This info was generated on: <br> <strong> {ts.strftime('%Y-%m-%d %H:%M:%S %Z%z')} </strong> </h4>")
27+
28+
cmd = 'git log -1 --pretty=format:"<h4>Git hash: <br><strong> %H </strong> <br> Date of commit: <br> <strong> %ai </strong></h4>"'
29+
subprocess.check_call(cmd, shell=True)
30+
31+
# when adding argparse, this should be a bit more obvious
32+
link = sys.argv[1] + sys.argv[2]
33+
print('<br> <h4>Build run: </h4> <a href="' + link + '"> ' + link + '</a> ')
34+
35+
link = "https://github.com/dataquest-dev/dspace-angular/releases/tag/" \
36+
+ RELEASE_TAG_BASE + "-" + datetime.now().strftime('%Y.%m.') + sys.argv[2]
37+
38+
print('<br> <br> <h4>Release link: </h4><a href="' + link + '"> ' + link + '</a> (if it does not work, then this is not an official release instance) ')

src/app/app-routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
4141
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
4242
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
4343
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
44+
import { STATIC_PAGE_PATH } from './static-page/static-page-routing-paths';
4445
import { viewTrackerResolver } from './statistics/angulartics/dspace/view-tracker.resolver';
4546
import { provideSubmissionState } from './submission/provide-submission-state';
4647
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
@@ -289,6 +290,11 @@ export const APP_ROUTES: Route[] = [
289290
.then((m) => m.ROUTES),
290291
canActivate: [notAuthenticatedGuard],
291292
},
293+
{
294+
path: STATIC_PAGE_PATH,
295+
loadChildren: () => import('./static-page/static-page-routes')
296+
.then((m) => m.ROUTES),
297+
},
292298
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent, data: { title: '404.page-not-found' } },
293299
],
294300
},
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import { HttpClient } from '@angular/common/http';
3+
import {
4+
Inject,
5+
Injectable,
6+
PLATFORM_ID,
7+
} from '@angular/core';
8+
import {
9+
firstValueFrom,
10+
of,
11+
} from 'rxjs';
12+
import {
13+
catchError,
14+
map,
15+
} from 'rxjs/operators';
16+
17+
import { LocaleService } from '../core/locale/locale.service';
18+
import {
19+
HTML_SUFFIX,
20+
STATIC_FILES_PROJECT_PATH,
21+
} from '../static-page/static-page-routing-paths';
22+
23+
interface HtmlContentResult {
24+
found: boolean;
25+
body: string;
26+
}
27+
28+
/**
29+
* Service for loading static `.html` files stored in the `/static-files` folder.
30+
*/
31+
@Injectable({
32+
providedIn: 'root',
33+
})
34+
export class HtmlContentService {
35+
constructor(
36+
private http: HttpClient,
37+
private localeService: LocaleService,
38+
@Inject(PLATFORM_ID) private platformId: object,
39+
) {}
40+
41+
private withSuffix(name: string): string {
42+
return name.endsWith(HTML_SUFFIX) ? name : name + HTML_SUFFIX;
43+
}
44+
45+
private fetch(url: string) {
46+
return this.http.get(url, { responseType: 'text' }).pipe(
47+
map((body): HtmlContentResult => ({ found: true, body })),
48+
catchError(() => of<HtmlContentResult>({ found: false, body: '' })),
49+
);
50+
}
51+
52+
/**
53+
* Load the html content for a file name, trying the current locale package first
54+
* (`static-files/<lang>/<file>.html`) and falling back to the default package
55+
* (`static-files/<file>.html`). Returns `undefined` when nothing was found.
56+
*
57+
* The files are fetched client-side only; during SSR this resolves to `undefined`
58+
* and the content is loaded after hydration.
59+
*/
60+
async getHtmlContentByPathAndLocale(fileName: string): Promise<string | undefined> {
61+
if (!isPlatformBrowser(this.platformId)) {
62+
return undefined;
63+
}
64+
65+
let language = await firstValueFrom(this.localeService.getCurrentLanguageCode());
66+
// Default language `en` lives in the non-translated (root) package.
67+
language = language === 'en' ? '' : language;
68+
69+
if (language) {
70+
const localized = await firstValueFrom(
71+
this.fetch(this.withSuffix(`${STATIC_FILES_PROJECT_PATH}/${language}/${fileName}`)),
72+
);
73+
if (localized.found) {
74+
return localized.body;
75+
}
76+
}
77+
78+
const fallback = await firstValueFrom(
79+
this.fetch(this.withSuffix(`${STATIC_FILES_PROJECT_PATH}/${fileName}`)),
80+
);
81+
return fallback.found ? fallback.body : undefined;
82+
}
83+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {
2+
Pipe,
3+
PipeTransform,
4+
} from '@angular/core';
5+
import {
6+
DomSanitizer,
7+
SafeHtml,
8+
} from '@angular/platform-browser';
9+
10+
/**
11+
* Pipe to keep html tags (e.g. `id`) when rendering a string via `[innerHTML]`.
12+
*/
13+
@Pipe({
14+
name: 'dsSafeHtml',
15+
})
16+
export class ClarinSafeHtmlPipe implements PipeTransform {
17+
constructor(private sanitized: DomSanitizer) {}
18+
19+
transform(htmlString: string): SafeHtml {
20+
return this.sanitized.bypassSecurityTrustHtml(htmlString ?? '');
21+
}
22+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Route } from '@angular/router';
2+
3+
import { StaticPageComponent } from './static-page.component';
4+
5+
export const ROUTES: Route[] = [
6+
{
7+
path: ':id',
8+
component: StaticPageComponent,
9+
},
10+
];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Constants for the `/static` route.
3+
*/
4+
export const STATIC_PAGE_PATH = 'static';
5+
6+
export const STATIC_FILES_PROJECT_PATH = 'static-files';
7+
8+
export const HTML_SUFFIX = '.html';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
@if (contentState === 'loading') {
2+
<div class="container text-center my-5">
3+
<div class="spinner-border" role="status">
4+
<span class="visually-hidden">{{ 'loading.default' | translate }}</span>
5+
</div>
6+
</div>
7+
}
8+
9+
<!-- Show static page content when found -->
10+
@if (contentState === 'found') {
11+
<div class="container">
12+
<div [innerHTML]="htmlContent | dsSafeHtml"></div>
13+
</div>
14+
}
15+
16+
<!-- Show 404 error when content not found -->
17+
@if (contentState === 'not-found') {
18+
<div class="container page-not-found">
19+
<h1>404</h1>
20+
<h2><small>{{ 'static-page.404.page-not-found' | translate }}</small></h2>
21+
<br/>
22+
<p>{{ 'static-page.404.help' | translate }}</p>
23+
<br/>
24+
<p class="text-center">
25+
<a routerLink="/home" class="btn btn-primary">{{ 'static-page.404.link.home-page' | translate }}</a>
26+
</p>
27+
</div>
28+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.page-not-found {
2+
text-align: center;
3+
margin-top: 3rem;
4+
margin-bottom: 3rem;
5+
}

0 commit comments

Comments
 (0)