Skip to content

Commit 20704bb

Browse files
imedinaclaude
andcommitted
wc: add Franklin and MobiDetails external links with user-managed API keys #TASK-8076
- Add Franklin and MobiDetails links to variant-browser-grid and variant-interpreter-grid action dropdowns - Create user-preferences component for managing external API keys stored in localStorage - Add Preferences tab to user profile page - MobiDetails links automatically include API key from localStorage when configured Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 444e7e0 commit 20704bb

File tree

6 files changed

+160
-1
lines changed

6 files changed

+160
-1
lines changed

src/core/bioinfo/bioinfo-utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,25 @@ export default class BioinfoUtils {
157157
return `https://genome.ucsc.edu/cgi-bin/hgTracks?db=${hg}&position=chr${region}`;
158158
case "VARSOME":
159159
return `https://varsome.com/variant/${assembly?.toUpperCase() === "GRCH38" ? "hg38" : "hg19"}/${BioinfoUtils.getVariantInVarsomeFormat(id)}`;
160+
case "FRANKLIN": {
161+
// Franklin format: chr{chr}-{pos}-{ref}-{alt}
162+
const [chr, pos, ref, alt] = id.split(":");
163+
return `https://franklin.genoox.com/clinical-db/variant/snp/chr${chr}-${pos}-${ref}-${alt}`;
164+
}
165+
case "MOBIDETAILS": {
166+
// MobiDetails VCF format: {chr}-{pos}-{ref}-{alt}
167+
const [chr, pos, ref, alt] = id.split(":");
168+
let url = `https://mobidetails.chu-montpellier.fr/api/variant/create_vcf_str?vcf_str=${chr}-${pos}-${ref}-${alt}&caller=browser`;
169+
try {
170+
const keys = JSON.parse(localStorage.getItem("iva.externalApiKeys") || "{}");
171+
if (keys.mobidetails) {
172+
url += `&api_key=${keys.mobidetails}`;
173+
}
174+
} catch (e) {
175+
// ignore localStorage errors
176+
}
177+
return url;
178+
}
160179
}
161180
}
162181

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
const USER_PROFILE_SETTINGS = {
22
// merge criterium: uses this array as filter for internal array.
3-
items: ["user-projects", "user-password-change"]
3+
items: ["user-projects", "user-password-change", "user-preferences"]
44
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {LitElement, html} from "lit";
2+
import NotificationUtils from "../commons/utils/notification-utils.js";
3+
import "../commons/forms/data-form.js";
4+
5+
export default class UserPreferences extends LitElement {
6+
7+
constructor() {
8+
super();
9+
this.#init();
10+
}
11+
12+
createRenderRoot() {
13+
return this;
14+
}
15+
16+
static get properties() {
17+
return {
18+
opencgaSession: {
19+
type: Object,
20+
},
21+
active: {
22+
type: Boolean,
23+
},
24+
};
25+
}
26+
27+
#init() {
28+
this.STORAGE_KEY = "iva.externalApiKeys";
29+
this._apiKeys = this.#loadFromStorage();
30+
this._config = this.getDefaultConfig();
31+
}
32+
33+
#loadFromStorage() {
34+
try {
35+
return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || "{}");
36+
} catch (e) {
37+
return {};
38+
}
39+
}
40+
41+
#saveToStorage() {
42+
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this._apiKeys));
43+
}
44+
45+
update(changedProperties) {
46+
if (changedProperties.has("active") && this.active) {
47+
this._apiKeys = this.#loadFromStorage();
48+
this._config = this.getDefaultConfig();
49+
}
50+
super.update(changedProperties);
51+
}
52+
53+
onFieldChange(e) {
54+
this._apiKeys = {...e.detail.data};
55+
this.requestUpdate();
56+
}
57+
58+
onSubmit() {
59+
this.#saveToStorage();
60+
NotificationUtils.dispatch(this, NotificationUtils.NOTIFY_SUCCESS, {
61+
message: "Preferences saved successfully",
62+
});
63+
}
64+
65+
onClear() {
66+
this._apiKeys = this.#loadFromStorage();
67+
this._config = this.getDefaultConfig();
68+
this.requestUpdate();
69+
}
70+
71+
render() {
72+
return html`
73+
<data-form
74+
.data="${this._apiKeys}"
75+
.config="${this._config}"
76+
@fieldChange="${e => this.onFieldChange(e)}"
77+
@submit="${() => this.onSubmit()}"
78+
@clear="${() => this.onClear()}">
79+
</data-form>
80+
`;
81+
}
82+
83+
getDefaultConfig() {
84+
return {
85+
title: "Preferences",
86+
display: {
87+
style: "margin: 10px",
88+
titleWidth: 3,
89+
defaultLayout: "horizontal",
90+
buttonOkText: "Save",
91+
},
92+
sections: [
93+
{
94+
title: "External API Keys",
95+
description: "Configure API keys for external services. Keys are stored locally in your browser and never sent to the server.",
96+
elements: [
97+
{
98+
title: "MobiDetails API Key",
99+
type: "input-text",
100+
field: "mobidetails",
101+
defaultValue: "",
102+
display: {
103+
helpMessage: "Get your API key from https://mobidetails.chu-montpellier.fr",
104+
},
105+
},
106+
],
107+
},
108+
],
109+
};
110+
}
111+
112+
}
113+
114+
customElements.define("user-preferences", UserPreferences);

src/webcomponents/user/user-profile.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "../commons/view/detail-tabs.js";
44
import "./user-info.js";
55
import "./user-projects.js";
66
import "./user-password-change.js";
7+
import "./user-preferences.js";
78
import ExtensionsManager from "../extensions-manager.js";
89

910
export default class UserProfile extends LitElement {
@@ -102,6 +103,19 @@ export default class UserProfile extends LitElement {
102103
</div>
103104
`,
104105
},
106+
{
107+
id: "user-preferences",
108+
name: "Preferences",
109+
active: false,
110+
render: (data, active, opencgaSession) => html`
111+
<div>
112+
<user-preferences
113+
.opencgaSession="${opencgaSession}"
114+
.active="${active}">
115+
</user-preferences>
116+
</div>
117+
`,
118+
},
105119
...ExtensionsManager.getViews("user-profile"),
106120
],
107121
};

src/webcomponents/variant/interpretation/variant-interpreter-grid.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,12 @@ export default class VariantInterpreterGrid extends LitElement {
10811081
<a target="_blank" class="dropdown-item" ${row.type === "COPY_NUMBER" ? "disabled" : ""} href="${BioinfoUtils.getVariantLink(row.id, "", "varsome", species, assembly)}">
10821082
<i class="fas fa-external-link-alt me-1"></i> Varsome
10831083
</a>
1084+
<a target="_blank" class="dropdown-item ${row.type !== "SNV" ? "disabled" : ""}" href="${BioinfoUtils.getVariantLink(row.id, "", "franklin", species, assembly)}">
1085+
<i class="fas fa-external-link-alt me-1"></i> Franklin
1086+
</a>
1087+
<a target="_blank" class="dropdown-item ${row.type !== "SNV" ? "disabled" : ""}" href="${BioinfoUtils.getVariantLink(row.id, "", "mobidetails", species, assembly)}">
1088+
<i class="fas fa-external-link-alt me-1"></i> MobiDetails
1089+
</a>
10841090
<div class="dropdown-header">CellBase Links</div>
10851091
${cellbaseLinks.join("")}
10861092
<div class="dropdown-header">External Genome Browsers</div>

src/webcomponents/variant/variant-browser-grid.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,12 @@ export default class VariantBrowserGrid extends LitElement {
860860
<a target="_blank" class="dropdown-item ${row.type === "COPY_NUMBER" ? "disabled" : ""}" href="${BioinfoUtils.getVariantLink(row.id, "", "varsome", species, assembly)}">
861861
<i class="fas fa-external-link-alt me-1"></i> Varsome
862862
</a>
863+
<a target="_blank" class="dropdown-item ${row.type !== "SNV" ? "disabled" : ""}" href="${BioinfoUtils.getVariantLink(row.id, "", "franklin", species, assembly)}">
864+
<i class="fas fa-external-link-alt me-1"></i> Franklin
865+
</a>
866+
<a target="_blank" class="dropdown-item ${row.type !== "SNV" ? "disabled" : ""}" href="${BioinfoUtils.getVariantLink(row.id, "", "mobidetails", species, assembly)}">
867+
<i class="fas fa-external-link-alt me-1"></i> MobiDetails
868+
</a>
863869
<div class="dropdown-header">CellBase Links</div>
864870
${cellbaseLinks.join("")}
865871
<div class="dropdown-header">External Genome Browsers</div>

0 commit comments

Comments
 (0)