From 13d9b836dc6918ac60219681cba956b643cac98a Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Wed, 22 Apr 2026 02:53:32 +0400 Subject: [PATCH 1/2] Cosmetic fixes --- src/App.vue | 4 +- src/app/core/net/api/RequestManager.ts | 182 +++++++++------- src/app/ui/MainUIComponent.vue | 205 +++++++++++------- .../ui/components/sidebar/LayerComponent.vue | 13 +- .../components/upper_ribbon/HeaderRibbon.vue | 44 ++-- src/assets/base.css | 4 +- 6 files changed, 276 insertions(+), 176 deletions(-) diff --git a/src/App.vue b/src/App.vue index a62aef3..9adce1e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -35,11 +35,11 @@ import MainUIComponent from "@/app/ui/MainUIComponent.vue"; diff --git a/src/app/core/net/api/RequestManager.ts b/src/app/core/net/api/RequestManager.ts index cc5e8f1..3288288 100644 --- a/src/app/core/net/api/RequestManager.ts +++ b/src/app/core/net/api/RequestManager.ts @@ -19,14 +19,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import assert from "assert"; import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; -import { ImageTile, Tile } from "ol"; -import TileState from "ol/TileState"; import type { AssemblyInfo } from "../../domain/AssemblyInfo"; import { AssemblyInfoDTO, - InboundDTO, OpenFileResponseDTO, VisualizationOptionsDTO, } from "../dto/dto"; @@ -43,7 +39,6 @@ import { TrackFeatureSearchResponseDTO, TrackQueryResponseDTO, TrackSummaryResponseDTO, - TilePOSTResponseDTO, WorkerSchedulerDiagnosticsResponseDTO, } from "../dto/responseDTO"; import type { OpenFileResponse } from "../netcommon"; @@ -148,41 +143,78 @@ export type SecondarySourceStatusResponse = { class RequestManager { constructor(public readonly networkManager: NetworkManager) {} - private normalizeAssemblyInfo(json: Record): Record { - const assemblyInfo = json["assemblyInfo"] as Record | undefined; + private notifyInboundPayload(payload: unknown): void { + if (!payload || typeof payload !== "object") { + return; + } + const json = payload as Record; + if (typeof json.error === "string" && json.error.trim().length > 0) { + toast.error(json.error); + } + if (typeof json.info === "string" && json.info.trim().length > 0) { + toast.success(json.info); + } + if (typeof json.message === "string" && json.message.trim().length > 0) { + toast(json.message); + } + if (typeof json.warning === "string" && json.warning.trim().length > 0) { + toast(json.warning, { + style: { + "background-color": "lightyellow", + color: "black", + }, + }); + } + } + + private normalizeAssemblyInfo( + json: Record + ): Record { + const assemblyInfo = json["assemblyInfo"] as + | Record + | undefined; return assemblyInfo ?? json; } - private parseSecondarySourceStatus(json: Record): SecondarySourceStatusResponse { + private parseSecondarySourceStatus( + json: Record + ): SecondarySourceStatusResponse { if (typeof json.error === "string" && json.error.trim().length > 0) { throw new Error(json.error); } const compatibilityRaw = (json.compatibility as Record | undefined) ?? undefined; - const compatibility: SecondarySourceCompatibility | undefined = compatibilityRaw - ? { - sameResolutions: Boolean(compatibilityRaw.sameResolutions ?? false), - sameMatrixSizes: Boolean(compatibilityRaw.sameMatrixSizes ?? false), - exactMatch: Boolean(compatibilityRaw.exactMatch ?? false), - primaryMaxBins: Number(compatibilityRaw.primaryMaxBins ?? 0), - secondaryMaxBins: Number(compatibilityRaw.secondaryMaxBins ?? 0), - primaryBinsByResolution: Array.isArray(compatibilityRaw.primaryBinsByResolution) - ? (compatibilityRaw.primaryBinsByResolution as unknown[]).map((value) => - Number(value ?? 0) - ) - : [], - secondaryBinsByResolution: Array.isArray(compatibilityRaw.secondaryBinsByResolution) - ? (compatibilityRaw.secondaryBinsByResolution as unknown[]).map((value) => - Number(value ?? 0) - ) - : [], - mismatchedResolutionOrders: Array.isArray(compatibilityRaw.mismatchedResolutionOrders) - ? (compatibilityRaw.mismatchedResolutionOrders as unknown[]).map((value) => - Number(value ?? 0) - ) - : [], - } - : undefined; + const compatibility: SecondarySourceCompatibility | undefined = + compatibilityRaw + ? { + sameResolutions: Boolean(compatibilityRaw.sameResolutions ?? false), + sameMatrixSizes: Boolean(compatibilityRaw.sameMatrixSizes ?? false), + exactMatch: Boolean(compatibilityRaw.exactMatch ?? false), + primaryMaxBins: Number(compatibilityRaw.primaryMaxBins ?? 0), + secondaryMaxBins: Number(compatibilityRaw.secondaryMaxBins ?? 0), + primaryBinsByResolution: Array.isArray( + compatibilityRaw.primaryBinsByResolution + ) + ? (compatibilityRaw.primaryBinsByResolution as unknown[]).map( + (value) => Number(value ?? 0) + ) + : [], + secondaryBinsByResolution: Array.isArray( + compatibilityRaw.secondaryBinsByResolution + ) + ? (compatibilityRaw.secondaryBinsByResolution as unknown[]).map( + (value) => Number(value ?? 0) + ) + : [], + mismatchedResolutionOrders: Array.isArray( + compatibilityRaw.mismatchedResolutionOrders + ) + ? (compatibilityRaw.mismatchedResolutionOrders as unknown[]).map( + (value) => Number(value ?? 0) + ) + : [], + } + : undefined; return { attached: Boolean(json.attached ?? false), filename: String(json.filename ?? ""), @@ -227,38 +259,21 @@ class RequestManager { req.data = JSON.parse(req.data); } catch (e) { throw new Error( - `Invalid response from ${request.requestPath}: ${req.data.slice(0, 200)}` + `Invalid response from ${request.requestPath}: ${req.data.slice( + 0, + 200 + )}` ); } } - if (req instanceof InboundDTO) { - if (req.error) { - toast.error(req.error); - } - if (req.info) { - toast.success(req.info); - } - if (req.message) { - toast(req.message); - } - if (req.warning) { - toast(req.warning, { - style: { - "background-color": "lightyellow", - color: "black", - }, - }); - } - } + this.notifyInboundPayload(req.data); return req; }) .catch((err) => { const errorToastStore = useErrorToastStore(); if (errorToastStore.requestErrorToastsEnabled) { const message = - err?.response?.data?.error ?? - err?.message ?? - "Request failed"; + err?.response?.data?.error ?? err?.message ?? "Request failed"; toast.error(message); } throw err; @@ -297,7 +312,9 @@ class RequestManager { filename: string, allowMismatch = false ): Promise { - return this.sendRequest(new OpenSecondarySourceRequest({ filename, allowMismatch })) + return this.sendRequest( + new OpenSecondarySourceRequest({ filename, allowMismatch }) + ) .then((response) => response.data as Record) .then((json) => this.parseSecondarySourceStatus(json)); } @@ -314,7 +331,9 @@ class RequestManager { assemblySource: "PRIMARY" | "SECONDARY"; assemblyInfo: AssemblyInfo; }> { - return this.sendRequest(new SetAssemblyInfoSourceRequest({ assemblySource })) + return this.sendRequest( + new SetAssemblyInfoSourceRequest({ assemblySource }) + ) .then((response) => response.data as Record) .then((json) => ({ assemblySource: @@ -331,7 +350,11 @@ class RequestManager { await this.sendRequest(new CloseFileRequest()); } - public async attachSession(): Promise<{ filename: string; fastaFilename: string; response: OpenFileResponse }> { + public async attachSession(): Promise<{ + filename: string; + fastaFilename: string; + response: OpenFileResponse; + }> { return this.sendRequest(new AttachSessionRequest()) .then((response) => response.data as Record) .then((json) => { @@ -365,7 +388,9 @@ class RequestManager { public async listFilesDetailed(): Promise { return this.sendRequest(new ListFilesDetailedRequest()) .then((response) => response.data as Record[]) - .then((items) => items.map((item) => new FileEntryResponseDTO(item).toEntity())); + .then((items) => + items.map((item) => new FileEntryResponseDTO(item).toEntity()) + ); } public async listCoolers(): Promise { @@ -408,7 +433,9 @@ class RequestManager { public async listTracks(): Promise { return this.sendRequest(new ListTracksRequest()) .then((response) => response.data as Record[]) - .then((items) => items.map((item) => new TrackSummaryResponseDTO(item).toEntity())); + .then((items) => + items.map((item) => new TrackSummaryResponseDTO(item).toEntity()) + ); } public async updateTrack( @@ -570,7 +597,9 @@ class RequestManager { trackId?: string, force = false ): Promise { - return this.sendRequest(new StartTracksPrecomputeRequest({ trackId, force })) + return this.sendRequest( + new StartTracksPrecomputeRequest({ trackId, force }) + ) .then((response) => response.data) .then((json) => new TracksPrecomputeStatusResponseDTO(json).toEntity()); } @@ -584,24 +613,29 @@ class RequestManager { public async getWorkerDiagnostics(): Promise { return this.sendRequest(new GetWorkerDiagnosticsRequest()) .then((response) => response.data) - .then((json) => new WorkerSchedulerDiagnosticsResponseDTO(json).toEntity()); + .then((json) => + new WorkerSchedulerDiagnosticsResponseDTO(json).toEntity() + ); } public async getRenderPipelineConfig(): Promise> { - return this.sendRequest(new GetRenderPipelineRequest()) - .then((response) => response.data as Record); + return this.sendRequest(new GetRenderPipelineRequest()).then( + (response) => response.data as Record + ); } public async setRenderPipelineConfig( config: Record ): Promise> { - return this.sendRequest(new SetRenderPipelineRequest(config)) - .then((response) => response.data as Record); + return this.sendRequest(new SetRenderPipelineRequest(config)).then( + (response) => response.data as Record + ); } public async resetRenderPipelineConfig(): Promise> { - return this.sendRequest(new ResetRenderPipelineRequest()) - .then((response) => response.data as Record); + return this.sendRequest(new ResetRenderPipelineRequest()).then( + (response) => response.data as Record + ); } public async listFASTAFiles(): Promise { @@ -609,7 +643,9 @@ class RequestManager { return response.data as string[]; } - public async linkFASTA(request: LinkFASTARequest): Promise { + public async linkFASTA( + request: LinkFASTARequest + ): Promise { return this.sendRequest(request) .then((response) => response.data) .then((json) => new FastaLinkResponseDTO(json).toEntity()) @@ -667,9 +703,7 @@ class RequestManager { scaffoldId: number, newName: string | null ): Promise { - return this.sendRequest( - new RenameScaffoldRequest({ scaffoldId, newName }) - ) + return this.sendRequest(new RenameScaffoldRequest({ scaffoldId, newName })) .then((response) => response.data) .then((json) => new AssemblyInfoDTO(this.normalizeAssemblyInfo(json)).toEntity() @@ -701,7 +735,9 @@ class RequestManager { .then((json) => Number(json.version ?? 0)); } - public async getBackendVersion(): Promise<{ version: string; webuiVersion?: string } | string> { + public async getBackendVersion(): Promise< + { version: string; webuiVersion?: string } | string + > { const host = this.networkManager.host.replace(/\/+$/, ""); return axios .get(`${host}/version`) diff --git a/src/app/ui/MainUIComponent.vue b/src/app/ui/MainUIComponent.vue index 706a0d5..080f355 100644 --- a/src/app/ui/MainUIComponent.vue +++ b/src/app/ui/MainUIComponent.vue @@ -35,7 +35,11 @@ @@ -90,7 +98,7 @@ import { ContactMapManager, // type ContactMapManagerOptions, } from "@/app/core/mapmanagers/ContactMapManager"; -import { onMounted, ref, watch, type Ref } from "vue"; +import { onMounted, onUnmounted, ref, watch, type Ref } from "vue"; import { NetworkManager } from "@/app/core/net/NetworkManager"; import defaultOptions from "@/app/core/visualization/colormap/default_options.json"; import { useVisualizationOptionsStore } from "@/app/stores/visualizationOptionsStore"; @@ -132,7 +140,7 @@ const { mapBackgroundColor } = storeToRefs(stylesStore); const sessionStore = useSessionStore(); const htmlElementReferencesStore = usehtmlElementReferencesStore(); -const { mapTarget, miniMapTarget } = storeToRefs(htmlElementReferencesStore); +const { miniMapTarget } = storeToRefs(htmlElementReferencesStore); const lastAgpFilename: Ref = ref(""); let openProgressTimer: number | undefined; const openProgressVisible = ref(false); @@ -144,6 +152,8 @@ function startOpenProgress() { if (openProgressTimer !== undefined) { return; } + openProgressStage.value = "starting"; + openProgressPct.value = 0; openProgressVisible.value = true; openProgressTimer = window.setInterval(() => { if (openProgressInFlight) return; @@ -152,7 +162,10 @@ function startOpenProgress() { .getOpenProgress() .then((p) => { openProgressStage.value = p.stage ?? "working"; - openProgressPct.value = Math.max(0, Math.min(100, Math.round((p.progress ?? 0) * 100))); + openProgressPct.value = Math.max( + 0, + Math.min(100, Math.round((p.progress ?? 0) * 100)) + ); }) .catch(() => undefined) .finally(() => { @@ -166,6 +179,7 @@ function stopOpenProgress() { window.clearInterval(openProgressTimer); openProgressTimer = undefined; } + openProgressInFlight = false; openProgressVisible.value = false; } @@ -173,7 +187,10 @@ function closeOpenProgress() { openProgressVisible.value = false; } -function safeColorTranslator(value: unknown, fallback: string): ColorTranslator { +function safeColorTranslator( + value: unknown, + fallback: string +): ColorTranslator { if (typeof value !== "string" || value.length > 128) { return new ColorTranslator(fallback, { legacyCSS: true }); } @@ -224,29 +241,35 @@ function onClosed() { function onAttached() { networkManager.requestManager .attachSession() - .then(({ filename: attachedName, fastaFilename: attachedFastaName, response }) => { - if (!attachedName) { - toast.error("No active session to attach"); - return; - } - mapManager.value?.dispose(); - filename.value = attachedName; - fastaFilename.value = attachedFastaName ?? ""; - const newManager = new ContactMapManager({ - response, + .then( + ({ filename: attachedName, - fastaFilename: attachedFastaName ?? "", - tileSize: tileSize.value, - contigBorderColor: contigBorderColor.value, - mapTargetSelector: "hic-contact-map", - networkManager: networkManager, - minimapTarget: miniMapTarget, - }); - mapManager.value = newManager; - networkManager.mapManager = mapManager.value; - newManager.initializeMap(); - toast.success("Attached to session " + attachedName); - }) + fastaFilename: attachedFastaName, + response, + }) => { + if (!attachedName) { + toast.error("No active session to attach"); + return; + } + mapManager.value?.dispose(); + filename.value = attachedName; + fastaFilename.value = attachedFastaName ?? ""; + const newManager = new ContactMapManager({ + response, + filename: attachedName, + fastaFilename: attachedFastaName ?? "", + tileSize: tileSize.value, + contigBorderColor: contigBorderColor.value, + mapTargetSelector: "hic-contact-map", + networkManager: networkManager, + minimapTarget: miniMapTarget, + }); + mapManager.value = newManager; + networkManager.mapManager = mapManager.value; + newManager.initializeMap(); + toast.success("Attached to session " + attachedName); + } + ) .catch((err) => { const message = err?.response?.data?.error ?? @@ -261,44 +284,47 @@ async function openFileWithOptions( ffname: string | undefined ): Promise { startOpenProgress(); - const openFileResponse = await networkManager.requestManager.openFile( - fname, - ffname - ); - mapManager.value?.dispose(); - const newManager = new ContactMapManager({ - response: openFileResponse, - filename: fname, - fastaFilename: ffname ?? "", - tileSize: tileSize.value, - contigBorderColor: contigBorderColor.value, - mapTargetSelector: "hic-contact-map", - networkManager: networkManager, - minimapTarget: miniMapTarget, - }); - mapManager.value = newManager; - networkManager.mapManager = mapManager.value; - newManager.initializeMap(); - applyDefaultVisualizationPreset(); - if (ffname && ffname.trim() !== "") { - try { - const linkResponse = await networkManager.requestManager.linkFASTA( - new LinkFASTARequest({ fastaFilename: ffname, allowMismatch: true }) - ); - linkResponse.warnings.forEach((warning) => - toast(warning, { - style: { - "background-color": "lightyellow", - color: "black", - }, - }) - ); - } catch (error) { - console.error(error); - toast.error("Failed to link FASTA file " + ffname); + try { + const openFileResponse = await networkManager.requestManager.openFile( + fname, + ffname + ); + mapManager.value?.dispose(); + const newManager = new ContactMapManager({ + response: openFileResponse, + filename: fname, + fastaFilename: ffname ?? "", + tileSize: tileSize.value, + contigBorderColor: contigBorderColor.value, + mapTargetSelector: "hic-contact-map", + networkManager: networkManager, + minimapTarget: miniMapTarget, + }); + mapManager.value = newManager; + networkManager.mapManager = mapManager.value; + newManager.initializeMap(); + applyDefaultVisualizationPreset(); + if (ffname && ffname.trim() !== "") { + try { + const linkResponse = await networkManager.requestManager.linkFASTA( + new LinkFASTARequest({ fastaFilename: ffname, allowMismatch: true }) + ); + linkResponse.warnings.forEach((warning) => + toast(warning, { + style: { + "background-color": "lightyellow", + color: "black", + }, + }) + ); + } catch (error) { + console.error(error); + toast.error("Failed to link FASTA file " + ffname); + } } + } finally { + stopOpenProgress(); } - stopOpenProgress(); } function displayNewMap() { @@ -326,12 +352,19 @@ function displayNewMap() { function applyDefaultVisualizationPreset() { const presets = - (defaultOptions as unknown as { - data?: { savedLocations?: unknown[]; savedVisualizationPresets?: unknown[] }; - }).data?.savedLocations ?? - (defaultOptions as unknown as { - data?: { savedVisualizationPresets?: unknown[] }; - }).data?.savedVisualizationPresets ?? + ( + defaultOptions as unknown as { + data?: { + savedLocations?: unknown[]; + savedVisualizationPresets?: unknown[]; + }; + } + ).data?.savedLocations ?? + ( + defaultOptions as unknown as { + data?: { savedVisualizationPresets?: unknown[] }; + } + ).data?.savedVisualizationPresets ?? []; if (!presets || presets.length === 0) { return; @@ -341,9 +374,12 @@ function applyDefaultVisualizationPreset() { const signalThresholds = first["signalThresholds"] as | { lowerSignalBound?: number; upperSignalBound?: number } | undefined; - const trackStyles = first["trackStyles"] as Record | undefined; + const trackStyles = first["trackStyles"] as + | Record + | undefined; const cmap = (opt["colormap"] as Record) ?? {}; - const startColor = (cmap["startColorRGBAString"] as string) ?? "rgba(0,255,0,0.0)"; + const startColor = + (cmap["startColorRGBAString"] as string) ?? "rgba(0,255,0,0.0)"; const endColor = (cmap["endColorRGBAString"] as string) ?? "rgba(0,96,0,1.0)"; const minSignal = (cmap["minSignal"] as number) ?? 0; const maxSignal = (cmap["maxSignal"] as number) ?? 1; @@ -360,7 +396,10 @@ function applyDefaultVisualizationPreset() { maxSignal ); let finalCmap = cmapObj; - if (signalThresholds && typeof signalThresholds.lowerSignalBound === "number") { + if ( + signalThresholds && + typeof signalThresholds.lowerSignalBound === "number" + ) { finalCmap = new SimpleLinearGradient( cmapObj.startColorRGBA, cmapObj.endColorRGBA, @@ -381,15 +420,17 @@ function applyDefaultVisualizationPreset() { ) ); const bg = (first["backgroundColor"] as string) ?? "rgba(255,255,255,1)"; - stylesStore.setMapBackground( - safeColorTranslator(bg, "rgba(255,255,255,1)") - ); + stylesStore.setMapBackground(safeColorTranslator(bg, "rgba(255,255,255,1)")); if (trackStyles && mapManager.value) { - mapManager.value.getLayersManager().applyTrackStylePreset(trackStyles as never); + mapManager.value + .getLayersManager() + .applyTrackStylePreset(trackStyles as never); } - mapManager.value?.visualizationManager.sendVisualizationOptionsToServer().then(() => { - mapManager.value?.reloadTiles(); - }); + mapManager.value?.visualizationManager + .sendVisualizationOptionsToServer() + .then(() => { + mapManager.value?.reloadTiles(); + }); } function onAgpLoaded(filename: string): void { @@ -639,6 +680,10 @@ onMounted(() => { syncUiChromePalette(); }); +onUnmounted(() => { + stopOpenProgress(); +}); + function onFileSelected(newFilename: string) { if (newFilename !== filename.value) { resetState(); diff --git a/src/app/ui/components/sidebar/LayerComponent.vue b/src/app/ui/components/sidebar/LayerComponent.vue index fce3b97..eac6873 100644 --- a/src/app/ui/components/sidebar/LayerComponent.vue +++ b/src/app/ui/components/sidebar/LayerComponent.vue @@ -147,7 +147,10 @@ - @@ -183,9 +186,9 @@ function getBaseColor(): ColorTranslator { try { if (props.getDefaultColor) { const style = props.getDefaultColor(); - const olColorString = style - ?.getStroke() - ?.getColor() as ColorLike as string | undefined; + const olColorString = style?.getStroke()?.getColor() as ColorLike as + | string + | undefined; if (olColorString) { return new ColorTranslator(olColorString, { legacyCSS: true }); } @@ -344,7 +347,7 @@ function resetStyle() { height: 20px; /* Body/Small */ - font-family: "Roboto", ui-sans-serif; + font-family: var(--hict-font-sans); font-style: normal; font-weight: 400; font-size: 13px; diff --git a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue index 7843bcd..19fc0a4 100644 --- a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue +++ b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue @@ -42,11 +42,18 @@
- + Searching genome features...
+ + @@ -69,7 +89,7 @@ - Right-click to add HiCT nodes only. Upper/Lower sinks accept color outputs; keep a colormap node before each sink. + Right-click to add HiCT nodes only. Upper/Lower sinks accept color outputs; use the same full-map graph on both sinks for overlays.
@@ -95,6 +115,17 @@ Import graph +