diff --git a/.github/workflows/deploy-helm.yml b/.github/workflows/deploy-helm.yml index cce906fa41dbc87880596854a3ee34ff6b77db32..cf98e4d1b0a11fbe42b8cbc0f7dfb3f1ba90eafb 100644 --- a/.github/workflows/deploy-helm.yml +++ b/.github/workflows/deploy-helm.yml @@ -9,6 +9,10 @@ on: IMAGE_TAG: required: true type: string + IMAGE_DIGEST: + required: false + type: string + default: 'unknown-digest' secrets: KUBECONFIG: @@ -32,12 +36,14 @@ jobs: helm --kubeconfig=$kubecfg_path \ upgrade \ --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ else echo "tag ${{ inputs.DEPLOYMENT_NAME }} not found. Install" helm --kubeconfig=$kubecfg_path \ install \ --set image.tag=${{ inputs.IMAGE_TAG }} \ + --set podLabels.image-digest=${{ inputs.IMAGE_DIGEST }} \ ${{ inputs.DEPLOYMENT_NAME }} .helm/siibra-explorer/ fi diff --git a/.github/workflows/docker_img.yml b/.github/workflows/docker_img.yml index 0e60c8d7bd01804eb748c5dd97baefc83632fc06..f9a9b2d212b7b0ff404addabd6f5f4794f4956b3 100644 --- a/.github/workflows/docker_img.yml +++ b/.github/workflows/docker_img.yml @@ -25,6 +25,9 @@ jobs: SIIBRA_API_RC: 'https://siibra-api-rc.apps.hbp.eu/v3_0' SIIBRA_API_LATEST: 'https://siibra-api-latest.apps-dev.hbp.eu/v3_0' + outputs: + IMAGE_DIGEST: ${{ steps.build-docker-image.outputs.IMAGE_DIGEST }} + steps: - uses: actions/checkout@v4 with: @@ -59,7 +62,8 @@ jobs: else echo "dev bulid, enable experimental features" fi - - name: 'Build docker image' + - id: 'build-docker-image' + name: 'Build docker image' run: | DOCKER_BUILT_TAG=${{ env.DOCKER_REGISTRY }}siibra-explorer:$BRANCH_NAME echo "Building $DOCKER_BUILT_TAG" @@ -73,6 +77,10 @@ jobs: echo "Successfully built $DOCKER_BUILT_TAG" echo "DOCKER_BUILT_TAG=$DOCKER_BUILT_TAG" >> $GITHUB_ENV + IMAGE_DIGEST=$(docker inspect --format='{{ index .RepoDigests 0 }}' $DOCKER_BUILT_TAG) + echo "Built image digest: $IMAGE_DIGEST" + echo "IMAGE_DIGEST=$IMAGE_DIGEST" >> $GITHUB_OUTPUT + - name: 'Push to docker registry' run: | echo "Login to docker registry" @@ -138,6 +146,19 @@ jobs: secrets: okd_token: ${{ secrets.OKD_PROD_SECRET }} + trigger-deploy-rc-rancher: + if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'rc' && success() }} + needs: + - build-docker-img + - setting-vars + uses: ./.github/workflows/deploy-helm.yml + with: + DEPLOYMENT_NAME: rc + IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} + secrets: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + trigger-deploy-master-rancher: if: ${{ needs.setting-vars.outputs.BRANCH_NAME == 'master' && success() }} needs: @@ -147,6 +168,7 @@ jobs: with: DEPLOYMENT_NAME: master IMAGE_TAG: ${{ needs.setting-vars.outputs.SXPLR_VERSION }} + IMAGE_DIGEST: ${{ needs.build-docker-img.outputs.IMAGE_DIGEST }} secrets: KUBECONFIG: ${{ secrets.KUBECONFIG }} diff --git a/.helm/siibra-explorer/Chart.yaml b/.helm/siibra-explorer/Chart.yaml index 301a17e4237e3c63762161df8c7bb7eed1d8904d..6cdf72464347e040f030ff28f7c1a5b4a036f457 100644 --- a/.helm/siibra-explorer/Chart.yaml +++ b/.helm/siibra-explorer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.1 +version: 0.1.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/src/atlasViewer/onhoverSegment.pipe.ts b/src/atlasViewer/onhoverSegment.pipe.ts deleted file mode 100644 index 5199a582a1ba2e5084d7097996652a492211a342..0000000000000000000000000000000000000000 --- a/src/atlasViewer/onhoverSegment.pipe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Pipe, PipeTransform, SecurityContext } from "@angular/core"; -import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; - -@Pipe({ - name: 'transformOnhoverSegment', -}) - -export class TransformOnhoverSegmentPipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { - - } - - private sanitizeHtml(inc: string): SafeHtml { - return this.sanitizer.sanitize(SecurityContext.HTML, inc) - } - - private getStatus(text: string) { - return ` <span class="text-muted">(${this.sanitizeHtml(text)})</span>` - } - - public transform(segment: any | number): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(( - ( this.sanitizeHtml(segment.name) || segment) + - (segment.status - ? this.getStatus(segment.status) - : '') - )) - } -} diff --git a/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html index 727aa06aebfeeb3f05ee0f19501ee34e29a297db..2cf12729686a4c940269ec9bb8ef80df03ebd417 100644 --- a/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html +++ b/src/features/compoundFeatureIndices/compoundFeatureIndices.template.html @@ -23,6 +23,7 @@ </table> <pointcloud-intents [points]="view.indices | filterForPoints" + (point-clicked)="handleOnClick($event)" [selected-template]="view.selectedTemplate"> </pointcloud-intents> </ng-template> diff --git a/src/features/compoundFeatureIndices/module.ts b/src/features/compoundFeatureIndices/module.ts index 387b0081f501bc3d03ce6debac1f802e52e208c1..1f0174e0e7ade54b49176c86b98f9f1e822ee132 100644 --- a/src/features/compoundFeatureIndices/module.ts +++ b/src/features/compoundFeatureIndices/module.ts @@ -5,6 +5,8 @@ import { CompoundFeatureIndices } from "./compoundFeatureIndices.component"; import { IndexToStrPipe } from "./idxToText.pipe"; import { IndexToIconPipe } from "./idxToIcon.pipe"; import { PointCloudIntents, FilterPointTransformer } from "src/features/pointcloud-intents"; +import { RENDER_CF_POINT, RenderCfPoint } from "../pointcloud-intents/intents.component"; + @NgModule({ imports: [ @@ -20,6 +22,16 @@ import { PointCloudIntents, FilterPointTransformer } from "src/features/pointclo ], exports: [ CompoundFeatureIndices, + ], + providers: [ + { + provide: RENDER_CF_POINT, + useFactory: () => { + const pipe = new IndexToStrPipe() + const renderCfPoint: RenderCfPoint = cfIndex => pipe.transform(cfIndex.index) + return renderCfPoint + } + } ] }) diff --git a/src/features/pointcloud-intents/intents.component.ts b/src/features/pointcloud-intents/intents.component.ts index 3df8d06bf00401c4a5716cdd1a1a3e26c2cbc7c4..c0c5af5bbf18d19d9c9d20676772943c4443c3eb 100644 --- a/src/features/pointcloud-intents/intents.component.ts +++ b/src/features/pointcloud-intents/intents.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Input, Output, inject } from "@angular/core"; +import { Component, EventEmitter, Inject, InjectionToken, Input, Optional, Output, inject } from "@angular/core"; import { BehaviorSubject, Observable, combineLatest } from "rxjs"; import { Point, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; import { PathReturn } from "src/atlasComponents/sapi/typeV3"; @@ -7,7 +7,8 @@ import { AngularMaterialModule } from "src/sharedModules"; import { DestroyDirective } from "src/util/directives/destroy.directive"; import { CFIndex } from "./util"; import { AnnotationLayer } from "src/atlasComponents/annotations"; -import { map, takeUntil } from "rxjs/operators"; +import { map, takeUntil, withLatestFrom } from "rxjs/operators"; +import { CLICK_INTERCEPTOR_INJECTOR, ClickInterceptor, HOVER_INTERCEPTOR_INJECTOR, HoverInterceptor, THoverConfig } from "src/util/injectionTokens"; type Intent = PathReturn<"/feature/{feature_id}/intents">['items'][number] @@ -61,33 +62,104 @@ export class PointCloudIntents { this.#selectedTemplate$.next(tmpl) } - spaceMatchedPoints$ = combineLatest([ + #spaceMatchedCfIndices$ = combineLatest([ this.#points$, this.#selectedTemplate$ ]).pipe( - map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id).map(v => v.index)) + map(([ points, selectedTemplate ]) => points.filter(p => p.index.spaceId === selectedTemplate?.id)) ) + #spaceMatchedAnnIdToCfIdx$ = this.#spaceMatchedCfIndices$.pipe( + map(indices => { + const idToIndexMap = new Map<string, CFIndex<Point>>() + for (const idx of indices){ + idToIndexMap.set( + serializeToId(idx.index).id, + idx + ) + } + return idToIndexMap + }) + ) - @Output('on-click') - onClick = new EventEmitter<Point>() + @Output('point-clicked') + pointClicked = new EventEmitter<CFIndex<Point>>() annLayer: AnnotationLayer - constructor(){ + constructor( + @Inject(RENDER_CF_POINT) render: RenderCfPoint, + @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor, + @Optional() @Inject(HOVER_INTERCEPTOR_INJECTOR) hoverInterceptor: HoverInterceptor, + ){ this.annLayer = new AnnotationLayer("intents", "#ff0000") - this.spaceMatchedPoints$.pipe( + this.#spaceMatchedCfIndices$.pipe( takeUntil(this.#destroy$) - ).subscribe(pts => { - const anns = pts.map(serializeToId) + ).subscribe(indices => { + const anns = indices.map(idx => serializeToId(idx.index)) this.annLayer.addAnnotation(anns) }, e => { console.error("error", e) }, () => { - console.log("dismissing!") this.annLayer.dispose() }) + + this.annLayer.onHover.pipe( + takeUntil(this.#destroy$), + withLatestFrom(this.#spaceMatchedAnnIdToCfIdx$), + ).subscribe(([hover, map]) => { + + if (hoverInterceptor && !!this.#hoveredMessage){ + const { remove } = hoverInterceptor + remove(this.#hoveredMessage) + this.#hoveredMessage = null + } + + this.#hoveredCfIndex = null + + if (!hover) { + return + } + + const idx = map.get(hover.id) + if (!idx) { + console.error(`Couldn't find AnnId: ${hover.id}`) + return + } + + this.#hoveredCfIndex = idx + + if (hoverInterceptor) { + const { append } = hoverInterceptor + const text = render(idx) + this.#hoveredMessage = { + message: `Hovering ${text}` + } + append(this.#hoveredMessage) + } + }) + + if (clickInterceptor) { + const { register, deregister } = clickInterceptor + const onClickHandler = this.onViewerClick.bind(this) + register(onClickHandler) + this.#destroy$.subscribe(() => deregister(onClickHandler)) + } } + onViewerClick(){ + if (this.#hoveredCfIndex) { + this.pointClicked.next(this.#hoveredCfIndex) + return false + } + return true + } + + #hoveredCfIndex: CFIndex<Point> = null + #hoveredMessage: THoverConfig = null + } + +export const RENDER_CF_POINT = new InjectionToken("RENDER_CF_POINT") +export type RenderCfPoint = (cfIndex: CFIndex<Point>) => string diff --git a/src/mouseoverModule/mouseover.directive.ts b/src/mouseoverModule/mouseover.directive.ts index fad1fbf852e3cadd9983ffdab1009fd6c6cbafde..a69fccd7630c0be2a35d7ec73d2dd1f0ed7a6d53 100644 --- a/src/mouseoverModule/mouseover.directive.ts +++ b/src/mouseoverModule/mouseover.directive.ts @@ -6,6 +6,7 @@ import { TOnHoverObj, temporalPositveScanFn } from "./util" import { ModularUserAnnotationToolService } from "src/atlasComponents/userAnnotations/tools/service"; import { userInteraction } from "src/state" import { arrayEqual } from "src/util/array" +import { MouseOverSvc } from "./service" @Directive({ selector: '[iav-mouse-hover]', @@ -14,6 +15,13 @@ import { arrayEqual } from "src/util/array" export class MouseHoverDirective { + /** + * TODO move + * - mousing over regions + * - hovering annotation + * - hovering voi feature + * to use hover interceptor + */ public currentOnHoverObs$: Observable<TOnHoverObj> = merge( this.store$.pipe( select(userInteraction.selectors.mousingOverRegions), @@ -58,6 +66,9 @@ export class MouseHoverDirective { constructor( private store$: Store<any>, private annotSvc: ModularUserAnnotationToolService, + private svc: MouseOverSvc, ) { } + + messages$ = this.svc.messages$ } diff --git a/src/mouseoverModule/mouseover.module.ts b/src/mouseoverModule/mouseover.module.ts index b5fbcc9feb9f04b1336d1df7209445144e569cc5..f4c54934842b01a3b656136c63d0706603a178c5 100644 --- a/src/mouseoverModule/mouseover.module.ts +++ b/src/mouseoverModule/mouseover.module.ts @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { TransformOnhoverSegmentPipe } from "src/atlasViewer/onhoverSegment.pipe"; +import { TransformOnhoverSegmentPipe } from "./transformOnhoverSegment.pipe"; import { MouseHoverDirective } from "./mouseover.directive"; import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; +import { HOVER_INTERCEPTOR_INJECTOR } from "src/util/injectionTokens"; +import { MouseOverSvc } from "./service"; @NgModule({ @@ -18,7 +20,20 @@ import { MouseOverConvertPipe } from "./mouseOverCvt.pipe"; MouseHoverDirective, TransformOnhoverSegmentPipe, MouseOverConvertPipe, + ], + providers: [ + MouseOverSvc, + { + provide: HOVER_INTERCEPTOR_INJECTOR, + useFactory: (svc: MouseOverSvc) => { + return { + append: svc.append.bind(svc), + remove: svc.remove.bind(svc), + } + }, + deps: [ MouseOverSvc ] + } ] }) -export class MouseoverModule{} \ No newline at end of file +export class MouseoverModule{} diff --git a/src/mouseoverModule/service.ts b/src/mouseoverModule/service.ts new file mode 100644 index 0000000000000000000000000000000000000000..eba057bcca83fab20a3e4afebee18de5c8d54efe --- /dev/null +++ b/src/mouseoverModule/service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { THoverConfig } from "src/util/injectionTokens"; + +@Injectable() +export class MouseOverSvc { + + #messages: THoverConfig[] = [] + + messages$ = new BehaviorSubject(this.#messages) + + set messages(messages: THoverConfig[]){ + this.#messages = messages + this.messages$.next(this.#messages) + } + + get messages(): THoverConfig[]{ + return this.#messages + } + + append(message: THoverConfig){ + this.messages = this.messages.concat(message) + } + remove(message: THoverConfig){ + this.messages = this.messages.filter(v => v !== message) + } +} diff --git a/src/util/injectionTokens.ts b/src/util/injectionTokens.ts index 250a8cce50b1279b6c4d63fbb3e8bcd517b96a33..3b62ed171a53a512307599c7bf28a79eed677a59 100644 --- a/src/util/injectionTokens.ts +++ b/src/util/injectionTokens.ts @@ -19,6 +19,19 @@ export interface ClickInterceptor{ deregister: (interceptorFunction: (ev: any) => any) => void } +export const HOVER_INTERCEPTOR_INJECTOR = new InjectionToken<HoverInterceptor>("HOVER_INTERCEPTOR_INJECTOR") + +export type THoverConfig = { + fontSet?: string + fontIcon?: string + message: string +} + +export interface HoverInterceptor { + append(message: THoverConfig): void + remove(message: THoverConfig): void +} + export const CONTEXT_MENU_ITEM_INJECTOR = new InjectionToken('CONTEXT_MENU_ITEM_INJECTOR') export type TContextMenu<T> = { diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html index 2526996c85a8c178080fecf32942ad40deafbefb..7530460b3b4dcac8387ef68bc6634575dbde595f 100644 --- a/src/viewerModule/viewerCmp/viewerCmp.template.html +++ b/src/viewerModule/viewerCmp/viewerCmp.template.html @@ -22,6 +22,17 @@ <span class="centered" matListItemIcon [class]="cvtOutput.icon.cls"></span> <span matListItemTitle>{{ cvtOutput.text }}</span> </mat-list-item> + + <mat-list-item *ngFor="let message of iavMouseHoverContextualBlock.messages$ | async" + class="h-auto"> + <ng-template [ngIf]="message.fontIcon && message.fontSet"> + <mat-icon matListItemIcon + [fontSet]="message.fontSet" + [fontIcon]="message.fontIcon"> + </mat-icon> + </ng-template> + <span matListItemTitle>{{ message.message }}</span> + </mat-list-item> </mat-list> </div>