@matdata/yasgui 5.5.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ts/src/ConfigExportImport.js +3 -0
- package/build/ts/src/ConfigExportImport.js.map +1 -1
- package/build/ts/src/OAuth2Utils.d.ts +18 -0
- package/build/ts/src/OAuth2Utils.js +214 -0
- package/build/ts/src/OAuth2Utils.js.map +1 -0
- package/build/ts/src/PersistentConfig.d.ts +3 -0
- package/build/ts/src/PersistentConfig.js +7 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +1 -1
- package/build/ts/src/Tab.js +116 -85
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +1 -0
- package/build/ts/src/TabSettingsModal.js +330 -27
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/defaults.js +1 -1
- package/build/ts/src/defaults.js.map +1 -1
- package/build/ts/src/index.d.ts +21 -6
- package/build/ts/src/index.js +7 -1
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +1 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +185 -157
- package/build/yasgui.min.js.map +4 -4
- package/package.json +3 -2
- package/src/ConfigExportImport.ts +3 -0
- package/src/OAuth2Utils.ts +315 -0
- package/src/PersistentConfig.ts +10 -0
- package/src/Tab.ts +191 -111
- package/src/TabSettingsModal.scss +70 -3
- package/src/TabSettingsModal.ts +400 -30
- package/src/defaults.ts +1 -1
- package/src/endpointSelect.scss +12 -0
- package/src/index.ts +42 -10
- package/src/tab.scss +1 -0
- package/src/themes.scss +1 -0
- package/src/version.ts +1 -1
package/src/Tab.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as shareLink from "./linkUtils";
|
|
|
9
9
|
import EndpointSelect from "./endpointSelect";
|
|
10
10
|
import "./tab.scss";
|
|
11
11
|
import { getRandomId, default as Yasgui, YasguiRequestConfig } from "./";
|
|
12
|
+
import * as OAuth2Utils from "./OAuth2Utils";
|
|
12
13
|
|
|
13
14
|
// Layout orientation toggle icons
|
|
14
15
|
const HORIZONTAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
|
|
@@ -20,9 +21,11 @@ const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
|
|
|
20
21
|
<rect x="2" y="2" width="20" height="8" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
21
22
|
<rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
|
|
22
23
|
</svg>`;
|
|
24
|
+
|
|
23
25
|
export interface PersistedJsonYasr extends YasrPersistentConfig {
|
|
24
26
|
responseSummary: Parser.ResponseSummary;
|
|
25
27
|
}
|
|
28
|
+
|
|
26
29
|
export interface PersistedJson {
|
|
27
30
|
name: string;
|
|
28
31
|
id: string;
|
|
@@ -37,6 +40,7 @@ export interface PersistedJson {
|
|
|
37
40
|
requestConfig: YasguiRequestConfig;
|
|
38
41
|
orientation?: "vertical" | "horizontal";
|
|
39
42
|
}
|
|
43
|
+
|
|
40
44
|
export interface Tab {
|
|
41
45
|
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
42
46
|
|
|
@@ -59,6 +63,7 @@ export interface Tab {
|
|
|
59
63
|
on(event: "autocompletionClose", listener: (tab: Tab) => void): this;
|
|
60
64
|
emit(event: "autocompletionClose", tab: Tab): boolean;
|
|
61
65
|
}
|
|
66
|
+
|
|
62
67
|
export class Tab extends EventEmitter {
|
|
63
68
|
private persistentJson: PersistedJson;
|
|
64
69
|
public yasgui: Yasgui;
|
|
@@ -73,6 +78,7 @@ export class Tab extends EventEmitter {
|
|
|
73
78
|
private settingsModal?: TabSettingsModal;
|
|
74
79
|
private currentOrientation: "vertical" | "horizontal";
|
|
75
80
|
private orientationToggleButton?: HTMLButtonElement;
|
|
81
|
+
|
|
76
82
|
constructor(yasgui: Yasgui, conf: PersistedJson) {
|
|
77
83
|
super();
|
|
78
84
|
if (!conf || conf.id === undefined) throw new Error("Expected a valid configuration to initialize tab with");
|
|
@@ -80,15 +86,19 @@ export class Tab extends EventEmitter {
|
|
|
80
86
|
this.persistentJson = conf;
|
|
81
87
|
this.currentOrientation = this.yasgui.config.orientation || "vertical";
|
|
82
88
|
}
|
|
89
|
+
|
|
83
90
|
public name() {
|
|
84
91
|
return this.persistentJson.name;
|
|
85
92
|
}
|
|
93
|
+
|
|
86
94
|
public getPersistedJson() {
|
|
87
95
|
return this.persistentJson;
|
|
88
96
|
}
|
|
97
|
+
|
|
89
98
|
public getId() {
|
|
90
99
|
return this.persistentJson.id;
|
|
91
100
|
}
|
|
101
|
+
|
|
92
102
|
private draw() {
|
|
93
103
|
if (this.rootEl) return; //aready drawn
|
|
94
104
|
this.rootEl = document.createElement("div");
|
|
@@ -128,10 +138,12 @@ export class Tab extends EventEmitter {
|
|
|
128
138
|
this.initYasr();
|
|
129
139
|
this.yasgui._setPanel(this.persistentJson.id, this.rootEl);
|
|
130
140
|
}
|
|
141
|
+
|
|
131
142
|
public hide() {
|
|
132
143
|
removeClass(this.rootEl, "active");
|
|
133
144
|
this.detachKeyboardListeners();
|
|
134
145
|
}
|
|
146
|
+
|
|
135
147
|
public show() {
|
|
136
148
|
this.draw();
|
|
137
149
|
addClass(this.rootEl, "active");
|
|
@@ -200,9 +212,11 @@ export class Tab extends EventEmitter {
|
|
|
200
212
|
private detachKeyboardListeners() {
|
|
201
213
|
document.removeEventListener("keydown", this.handleKeyDown);
|
|
202
214
|
}
|
|
215
|
+
|
|
203
216
|
public select() {
|
|
204
217
|
this.yasgui.selectTabId(this.persistentJson.id);
|
|
205
218
|
}
|
|
219
|
+
|
|
206
220
|
public close() {
|
|
207
221
|
this.detachKeyboardListeners();
|
|
208
222
|
if (this.yasqe) this.yasqe.abortQuery();
|
|
@@ -222,12 +236,14 @@ export class Tab extends EventEmitter {
|
|
|
222
236
|
this.yasgui.tabElements.get(this.persistentJson.id).delete();
|
|
223
237
|
delete this.yasgui._tabs[this.persistentJson.id];
|
|
224
238
|
}
|
|
239
|
+
|
|
225
240
|
public getQuery() {
|
|
226
241
|
if (!this.yasqe) {
|
|
227
242
|
throw new Error("Cannot get value from uninitialized editor");
|
|
228
243
|
}
|
|
229
244
|
return this.yasqe?.getValue();
|
|
230
245
|
}
|
|
246
|
+
|
|
231
247
|
public setQuery(query: string) {
|
|
232
248
|
if (!this.yasqe) {
|
|
233
249
|
throw new Error("Cannot set value for uninitialized editor");
|
|
@@ -237,9 +253,11 @@ export class Tab extends EventEmitter {
|
|
|
237
253
|
this.emit("change", this, this.persistentJson);
|
|
238
254
|
return this;
|
|
239
255
|
}
|
|
256
|
+
|
|
240
257
|
public getRequestConfig() {
|
|
241
258
|
return this.persistentJson.requestConfig;
|
|
242
259
|
}
|
|
260
|
+
|
|
243
261
|
private initControlbar() {
|
|
244
262
|
this.initOrientationToggle();
|
|
245
263
|
this.initEndpointSelectField();
|
|
@@ -313,12 +331,15 @@ export class Tab extends EventEmitter {
|
|
|
313
331
|
}
|
|
314
332
|
}
|
|
315
333
|
}
|
|
334
|
+
|
|
316
335
|
public getYasqe() {
|
|
317
336
|
return this.yasqe;
|
|
318
337
|
}
|
|
338
|
+
|
|
319
339
|
public getYasr() {
|
|
320
340
|
return this.yasr;
|
|
321
341
|
}
|
|
342
|
+
|
|
322
343
|
private initTabSettingsMenu() {
|
|
323
344
|
if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing tab settings");
|
|
324
345
|
this.settingsModal = new TabSettingsModal(this, this.controlBarEl);
|
|
@@ -397,26 +418,11 @@ export class Tab extends EventEmitter {
|
|
|
397
418
|
});
|
|
398
419
|
}
|
|
399
420
|
|
|
400
|
-
private checkEndpointForCors(endpoint: string) {
|
|
401
|
-
if (this.yasgui.config.corsProxy && !(endpoint in Yasgui.corsEnabled)) {
|
|
402
|
-
const askUrl = new URL(endpoint);
|
|
403
|
-
askUrl.searchParams.append("query", "ASK {?x ?y ?z}");
|
|
404
|
-
fetch(askUrl.toString())
|
|
405
|
-
.then(() => {
|
|
406
|
-
Yasgui.corsEnabled[endpoint] = true;
|
|
407
|
-
})
|
|
408
|
-
.catch((e) => {
|
|
409
|
-
// CORS error throws `TypeError: NetworkError when attempting to fetch resource.`
|
|
410
|
-
Yasgui.corsEnabled[endpoint] = e instanceof TypeError ? false : true;
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
421
|
public setEndpoint(endpoint: string, endpointHistory?: string[]) {
|
|
415
422
|
if (endpoint) endpoint = endpoint.trim();
|
|
416
423
|
if (endpointHistory && !eq(endpointHistory, this.yasgui.persistentConfig.getEndpointHistory())) {
|
|
417
424
|
this.yasgui.emit("endpointHistoryChange", this.yasgui, endpointHistory);
|
|
418
425
|
}
|
|
419
|
-
this.checkEndpointForCors(endpoint); //little cost in checking this as we're caching the check results
|
|
420
426
|
|
|
421
427
|
if (this.persistentJson.requestConfig.endpoint !== endpoint) {
|
|
422
428
|
this.persistentJson.requestConfig.endpoint = endpoint;
|
|
@@ -433,9 +439,11 @@ export class Tab extends EventEmitter {
|
|
|
433
439
|
}
|
|
434
440
|
return this;
|
|
435
441
|
}
|
|
442
|
+
|
|
436
443
|
public getEndpoint(): string {
|
|
437
444
|
return getAsValue(this.persistentJson.requestConfig.endpoint, this.yasgui);
|
|
438
445
|
}
|
|
446
|
+
|
|
439
447
|
/**
|
|
440
448
|
* Updates the position of the Tab's contextmenu
|
|
441
449
|
* Useful for when being scrolled
|
|
@@ -443,21 +451,26 @@ export class Tab extends EventEmitter {
|
|
|
443
451
|
public updateContextMenu(): void {
|
|
444
452
|
this.getTabListEl().redrawContextMenu();
|
|
445
453
|
}
|
|
454
|
+
|
|
446
455
|
public getShareableLink(baseURL?: string): string {
|
|
447
456
|
return shareLink.createShareLink(baseURL || window.location.href, this);
|
|
448
457
|
}
|
|
458
|
+
|
|
449
459
|
public getShareObject() {
|
|
450
460
|
return shareLink.createShareConfig(this);
|
|
451
461
|
}
|
|
462
|
+
|
|
452
463
|
private getTabListEl(): TabListEl {
|
|
453
464
|
return this.yasgui.tabElements.get(this.persistentJson.id);
|
|
454
465
|
}
|
|
466
|
+
|
|
455
467
|
public setName(newName: string) {
|
|
456
468
|
this.getTabListEl().rename(newName);
|
|
457
469
|
this.persistentJson.name = newName;
|
|
458
470
|
this.emit("change", this, this.persistentJson);
|
|
459
471
|
return this;
|
|
460
472
|
}
|
|
473
|
+
|
|
461
474
|
public hasResults() {
|
|
462
475
|
return !!this.yasr?.results;
|
|
463
476
|
}
|
|
@@ -465,10 +478,18 @@ export class Tab extends EventEmitter {
|
|
|
465
478
|
public getName() {
|
|
466
479
|
return this.persistentJson.name;
|
|
467
480
|
}
|
|
468
|
-
public query(): Promise<any> {
|
|
481
|
+
public async query(): Promise<any> {
|
|
469
482
|
if (!this.yasqe) return Promise.reject(new Error("No yasqe editor initialized"));
|
|
483
|
+
|
|
484
|
+
// Check and refresh OAuth 2.0 token if needed
|
|
485
|
+
const tokenValid = await this.ensureOAuth2TokenValid();
|
|
486
|
+
if (!tokenValid) {
|
|
487
|
+
return Promise.reject(new Error("OAuth 2.0 authentication failed"));
|
|
488
|
+
}
|
|
489
|
+
|
|
470
490
|
return this.yasqe.query();
|
|
471
491
|
}
|
|
492
|
+
|
|
472
493
|
public setRequestConfig(requestConfig: Partial<YasguiRequestConfig>) {
|
|
473
494
|
this.persistentJson.requestConfig = {
|
|
474
495
|
...this.persistentJson.requestConfig,
|
|
@@ -490,16 +511,109 @@ export class Tab extends EventEmitter {
|
|
|
490
511
|
if (!endpointConfig || !endpointConfig.authentication) return undefined;
|
|
491
512
|
|
|
492
513
|
// Convert endpoint auth to requestConfig format
|
|
493
|
-
|
|
514
|
+
const auth = endpointConfig.authentication;
|
|
515
|
+
if (auth.type === "basic") {
|
|
516
|
+
return {
|
|
517
|
+
type: "basic" as const,
|
|
518
|
+
config: {
|
|
519
|
+
username: auth.username,
|
|
520
|
+
password: auth.password,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
} else if (auth.type === "bearer") {
|
|
494
524
|
return {
|
|
495
|
-
|
|
496
|
-
|
|
525
|
+
type: "bearer" as const,
|
|
526
|
+
config: {
|
|
527
|
+
token: auth.token,
|
|
528
|
+
},
|
|
497
529
|
};
|
|
530
|
+
} else if (auth.type === "apiKey") {
|
|
531
|
+
return {
|
|
532
|
+
type: "apiKey" as const,
|
|
533
|
+
config: {
|
|
534
|
+
headerName: auth.headerName,
|
|
535
|
+
apiKey: auth.apiKey,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
} else if (auth.type === "oauth2") {
|
|
539
|
+
// For OAuth 2.0, return the current access token and ID token
|
|
540
|
+
// Token refresh is handled separately before query execution
|
|
541
|
+
if (auth.accessToken) {
|
|
542
|
+
return {
|
|
543
|
+
type: "oauth2" as const,
|
|
544
|
+
config: {
|
|
545
|
+
accessToken: auth.accessToken,
|
|
546
|
+
idToken: auth.idToken,
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
498
550
|
}
|
|
499
551
|
|
|
500
552
|
return undefined;
|
|
501
553
|
}
|
|
502
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Check and refresh OAuth 2.0 token if needed
|
|
557
|
+
* Should be called before query execution
|
|
558
|
+
*/
|
|
559
|
+
private async ensureOAuth2TokenValid(): Promise<boolean> {
|
|
560
|
+
const endpoint = this.getEndpoint();
|
|
561
|
+
if (!endpoint) return true;
|
|
562
|
+
|
|
563
|
+
const endpointConfig = this.yasgui.persistentConfig.getEndpointConfig(endpoint);
|
|
564
|
+
if (!endpointConfig || !endpointConfig.authentication) return true;
|
|
565
|
+
|
|
566
|
+
const auth = endpointConfig.authentication;
|
|
567
|
+
if (auth.type !== "oauth2") return true;
|
|
568
|
+
|
|
569
|
+
// Check if token is expired
|
|
570
|
+
if (OAuth2Utils.isTokenExpired(auth.tokenExpiry)) {
|
|
571
|
+
// Try to refresh the token if we have a refresh token
|
|
572
|
+
if (auth.refreshToken) {
|
|
573
|
+
try {
|
|
574
|
+
const tokenResponse = await OAuth2Utils.refreshOAuth2Token(
|
|
575
|
+
{
|
|
576
|
+
clientId: auth.clientId,
|
|
577
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
578
|
+
},
|
|
579
|
+
auth.refreshToken,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
const tokenExpiry = OAuth2Utils.calculateTokenExpiry(tokenResponse.expires_in);
|
|
583
|
+
|
|
584
|
+
// Update stored authentication with new tokens
|
|
585
|
+
this.yasgui.persistentConfig.addOrUpdateEndpoint(endpoint, {
|
|
586
|
+
authentication: {
|
|
587
|
+
...auth,
|
|
588
|
+
accessToken: tokenResponse.access_token,
|
|
589
|
+
idToken: tokenResponse.id_token,
|
|
590
|
+
refreshToken: tokenResponse.refresh_token || auth.refreshToken,
|
|
591
|
+
tokenExpiry,
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return true;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error("Failed to refresh OAuth 2.0 token:", error);
|
|
598
|
+
// Token refresh failed, user needs to re-authenticate
|
|
599
|
+
alert(
|
|
600
|
+
"Your OAuth 2.0 session has expired and could not be refreshed. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.",
|
|
601
|
+
);
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
// No refresh token available, user needs to re-authenticate
|
|
606
|
+
alert(
|
|
607
|
+
"Your OAuth 2.0 session has expired. Please re-authenticate by clicking the Settings button (gear icon) and selecting the SPARQL Endpoints tab.",
|
|
608
|
+
);
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Token is still valid
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
503
617
|
/**
|
|
504
618
|
* The Yasgui configuration object may contain a custom request config
|
|
505
619
|
* This request config object can contain getter functions, or plain json
|
|
@@ -539,6 +653,8 @@ export class Tab extends EventEmitter {
|
|
|
539
653
|
persistenceId: null, //yasgui handles persistent storing
|
|
540
654
|
consumeShareLink: null, //not handled by this tab, but by parent yasgui instance
|
|
541
655
|
createShareableLink: () => this.getShareableLink(),
|
|
656
|
+
// Use global showSnippetsBar setting if it exists
|
|
657
|
+
showSnippetsBar: this.yasgui.config.showSnippetsBar !== false,
|
|
542
658
|
requestConfig: () => {
|
|
543
659
|
const processedReqConfig: YasguiRequestConfig = {
|
|
544
660
|
//setting defaults
|
|
@@ -562,24 +678,20 @@ export class Tab extends EventEmitter {
|
|
|
562
678
|
};
|
|
563
679
|
|
|
564
680
|
// Inject authentication from endpoint-based storage
|
|
565
|
-
// Only inject endpoint-based auth if
|
|
681
|
+
// Only inject endpoint-based auth if the corresponding auth type is not already set
|
|
566
682
|
const endpointAuth = this.getAuthForCurrentEndpoint();
|
|
567
|
-
if (endpointAuth
|
|
568
|
-
processedReqConfig.basicAuth
|
|
683
|
+
if (endpointAuth) {
|
|
684
|
+
if (endpointAuth.type === "basic" && typeof processedReqConfig.basicAuth === "undefined") {
|
|
685
|
+
processedReqConfig.basicAuth = endpointAuth.config;
|
|
686
|
+
} else if (endpointAuth.type === "bearer" && typeof processedReqConfig.bearerAuth === "undefined") {
|
|
687
|
+
processedReqConfig.bearerAuth = endpointAuth.config;
|
|
688
|
+
} else if (endpointAuth.type === "apiKey" && typeof processedReqConfig.apiKeyAuth === "undefined") {
|
|
689
|
+
processedReqConfig.apiKeyAuth = endpointAuth.config;
|
|
690
|
+
} else if (endpointAuth.type === "oauth2" && typeof processedReqConfig.oauth2Auth === "undefined") {
|
|
691
|
+
processedReqConfig.oauth2Auth = endpointAuth.config;
|
|
692
|
+
}
|
|
569
693
|
}
|
|
570
694
|
|
|
571
|
-
if (this.yasgui.config.corsProxy && !Yasgui.corsEnabled[this.getEndpoint()]) {
|
|
572
|
-
return {
|
|
573
|
-
...processedReqConfig,
|
|
574
|
-
args: [
|
|
575
|
-
...(Array.isArray(processedReqConfig.args) ? processedReqConfig.args : []),
|
|
576
|
-
{ name: "endpoint", value: this.getEndpoint() },
|
|
577
|
-
{ name: "method", value: this.persistentJson.requestConfig.method },
|
|
578
|
-
],
|
|
579
|
-
method: "POST",
|
|
580
|
-
endpoint: this.yasgui.config.corsProxy,
|
|
581
|
-
} as PlainRequestConfig;
|
|
582
|
-
}
|
|
583
695
|
return processedReqConfig as PlainRequestConfig;
|
|
584
696
|
},
|
|
585
697
|
};
|
|
@@ -608,6 +720,7 @@ export class Tab extends EventEmitter {
|
|
|
608
720
|
// Add Ctrl+Click handler for URIs
|
|
609
721
|
this.attachYasqeMouseHandler();
|
|
610
722
|
}
|
|
723
|
+
|
|
611
724
|
private destroyYasqe() {
|
|
612
725
|
// As Yasqe extends of CM instead of eventEmitter, it doesn't expose the removeAllListeners function, so we should unregister all events manually
|
|
613
726
|
this.yasqe?.off("blur", this.handleYasqeBlur);
|
|
@@ -622,12 +735,14 @@ export class Tab extends EventEmitter {
|
|
|
622
735
|
this.yasqe?.destroy();
|
|
623
736
|
this.yasqe = undefined;
|
|
624
737
|
}
|
|
738
|
+
|
|
625
739
|
handleYasqeBlur = (yasqe: Yasqe) => {
|
|
626
740
|
this.persistentJson.yasqe.value = yasqe.getValue();
|
|
627
741
|
// Capture prefixes from query if auto-capture is enabled
|
|
628
742
|
this.settingsModal?.capturePrefixesFromQuery();
|
|
629
743
|
this.emit("change", this, this.persistentJson);
|
|
630
744
|
};
|
|
745
|
+
|
|
631
746
|
handleYasqeQuery = (yasqe: Yasqe) => {
|
|
632
747
|
//the blur event might not have fired (e.g. when pressing ctrl-enter). So, we'd like to persist the query as well if needed
|
|
633
748
|
if (yasqe.getValue() !== this.persistentJson.yasqe.value) {
|
|
@@ -636,6 +751,7 @@ export class Tab extends EventEmitter {
|
|
|
636
751
|
}
|
|
637
752
|
this.emit("query", this);
|
|
638
753
|
};
|
|
754
|
+
|
|
639
755
|
handleYasqeQueryAbort = () => {
|
|
640
756
|
this.emit("queryAbort", this);
|
|
641
757
|
// Hide loading indicator in Yasr
|
|
@@ -643,6 +759,7 @@ export class Tab extends EventEmitter {
|
|
|
643
759
|
this.yasr.hideLoading();
|
|
644
760
|
}
|
|
645
761
|
};
|
|
762
|
+
|
|
646
763
|
handleYasqeQueryBefore = () => {
|
|
647
764
|
this.emit("queryBefore", this);
|
|
648
765
|
// Show loading indicator in Yasr
|
|
@@ -650,16 +767,20 @@ export class Tab extends EventEmitter {
|
|
|
650
767
|
this.yasr.showLoading();
|
|
651
768
|
}
|
|
652
769
|
};
|
|
770
|
+
|
|
653
771
|
handleYasqeResize = (_yasqe: Yasqe, newSize: string) => {
|
|
654
772
|
this.persistentJson.yasqe.editorHeight = newSize;
|
|
655
773
|
this.emit("change", this, this.persistentJson);
|
|
656
774
|
};
|
|
775
|
+
|
|
657
776
|
handleAutocompletionShown = (_yasqe: Yasqe, widget: string) => {
|
|
658
777
|
this.emit("autocompletionShown", this, widget);
|
|
659
778
|
};
|
|
779
|
+
|
|
660
780
|
handleAutocompletionClose = (_yasqe: Yasqe) => {
|
|
661
781
|
this.emit("autocompletionClose", this);
|
|
662
782
|
};
|
|
783
|
+
|
|
663
784
|
handleQueryResponse = (_yasqe: Yasqe, response: any, duration: number) => {
|
|
664
785
|
this.emit("queryResponse", this);
|
|
665
786
|
if (!this.yasr) throw new Error("Resultset visualizer not initialized. Cannot draw results");
|
|
@@ -736,7 +857,8 @@ WHERE {
|
|
|
736
857
|
} LIMIT 1000`;
|
|
737
858
|
|
|
738
859
|
// Execute query in background without changing editor content
|
|
739
|
-
|
|
860
|
+
// Note: void operator is intentional - errors are handled in the catch block of executeBackgroundQuery
|
|
861
|
+
void this.executeBackgroundQuery(constructQuery);
|
|
740
862
|
};
|
|
741
863
|
|
|
742
864
|
private async executeBackgroundQuery(query: string) {
|
|
@@ -747,75 +869,21 @@ WHERE {
|
|
|
747
869
|
this.yasr.showLoading();
|
|
748
870
|
this.emit("queryBefore", this);
|
|
749
871
|
|
|
750
|
-
//
|
|
751
|
-
const
|
|
752
|
-
const config = typeof requestConfig === "function" ? requestConfig(this.yasqe) : requestConfig;
|
|
753
|
-
|
|
754
|
-
if (!config.endpoint) {
|
|
755
|
-
throw new Error("No endpoint configured");
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const endpoint = typeof config.endpoint === "function" ? config.endpoint(this.yasqe) : config.endpoint;
|
|
759
|
-
const method = typeof config.method === "function" ? config.method(this.yasqe) : config.method || "POST";
|
|
760
|
-
const headers = typeof config.headers === "function" ? config.headers(this.yasqe) : config.headers || {};
|
|
761
|
-
|
|
762
|
-
// Prepare request
|
|
763
|
-
const searchParams = new URLSearchParams();
|
|
764
|
-
searchParams.append("query", query);
|
|
765
|
-
|
|
766
|
-
// Add any additional args
|
|
767
|
-
if (config.args && Array.isArray(config.args)) {
|
|
768
|
-
config.args.forEach((arg: any) => {
|
|
769
|
-
if (arg.name && arg.value) {
|
|
770
|
-
searchParams.append(arg.name, arg.value);
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
}
|
|
872
|
+
// Track query execution time
|
|
873
|
+
const startTime = Date.now();
|
|
774
874
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
875
|
+
// Use yasqe's executeQuery with custom query and accept header
|
|
876
|
+
const queryResponse = await Yasqe.Sparql.executeQuery(
|
|
877
|
+
this.yasqe,
|
|
878
|
+
undefined, // Use default config
|
|
879
|
+
{
|
|
880
|
+
customQuery: query,
|
|
881
|
+
customAccept: "text/turtle",
|
|
780
882
|
},
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
let url = endpoint;
|
|
784
|
-
if (method === "POST") {
|
|
785
|
-
fetchOptions.headers = {
|
|
786
|
-
...fetchOptions.headers,
|
|
787
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
788
|
-
};
|
|
789
|
-
fetchOptions.body = searchParams.toString();
|
|
790
|
-
} else {
|
|
791
|
-
const urlObj = new URL(endpoint);
|
|
792
|
-
searchParams.forEach((value, key) => {
|
|
793
|
-
urlObj.searchParams.append(key, value);
|
|
794
|
-
});
|
|
795
|
-
url = urlObj.toString();
|
|
796
|
-
}
|
|
883
|
+
);
|
|
797
884
|
|
|
798
|
-
const startTime = Date.now();
|
|
799
|
-
const response = await fetch(url, fetchOptions);
|
|
800
885
|
const duration = Date.now() - startTime;
|
|
801
886
|
|
|
802
|
-
if (!response.ok) {
|
|
803
|
-
throw new Error(`Query failed: ${response.statusText}`);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
const result = await response.text();
|
|
807
|
-
|
|
808
|
-
// Create a query response object similar to what Yasqe produces
|
|
809
|
-
// This includes headers so the Parser can detect the content type
|
|
810
|
-
const queryResponse = {
|
|
811
|
-
ok: response.ok,
|
|
812
|
-
status: response.status,
|
|
813
|
-
statusText: response.statusText,
|
|
814
|
-
headers: response.headers,
|
|
815
|
-
type: response.type,
|
|
816
|
-
content: result,
|
|
817
|
-
};
|
|
818
|
-
|
|
819
887
|
// Set the response in Yasr
|
|
820
888
|
this.yasr.setResponse(queryResponse, duration);
|
|
821
889
|
|
|
@@ -831,10 +899,17 @@ WHERE {
|
|
|
831
899
|
console.error("Background query failed:", error);
|
|
832
900
|
if (this.yasr) {
|
|
833
901
|
this.yasr.hideLoading();
|
|
834
|
-
// Set error response
|
|
902
|
+
// Set error response with detailed HTTP status if available
|
|
903
|
+
const errorObj: any = error;
|
|
904
|
+
let errorText = error instanceof Error ? error.message : String(error);
|
|
905
|
+
|
|
835
906
|
this.yasr.setResponse(
|
|
836
907
|
{
|
|
837
|
-
error:
|
|
908
|
+
error: {
|
|
909
|
+
status: errorObj.status,
|
|
910
|
+
statusText: errorObj.statusText || (error instanceof Error ? error.name : undefined),
|
|
911
|
+
text: errorText,
|
|
912
|
+
},
|
|
838
913
|
},
|
|
839
914
|
0,
|
|
840
915
|
);
|
|
@@ -910,6 +985,7 @@ WHERE {
|
|
|
910
985
|
this.emit("change", this, this.persistentJson);
|
|
911
986
|
});
|
|
912
987
|
}
|
|
988
|
+
|
|
913
989
|
destroy() {
|
|
914
990
|
this.removeAllListeners();
|
|
915
991
|
this.settingsModal?.destroy();
|
|
@@ -919,6 +995,7 @@ WHERE {
|
|
|
919
995
|
this.yasr = undefined;
|
|
920
996
|
this.destroyYasqe();
|
|
921
997
|
}
|
|
998
|
+
|
|
922
999
|
public static getDefaults(yasgui?: Yasgui): PersistedJson {
|
|
923
1000
|
return {
|
|
924
1001
|
yasqe: {
|
|
@@ -948,11 +1025,21 @@ const safeEndpoint = (endpoint: string): string => {
|
|
|
948
1025
|
|
|
949
1026
|
function getCorsErrorRenderer(tab: Tab) {
|
|
950
1027
|
return async (error: Parser.ErrorSummary): Promise<HTMLElement | undefined> => {
|
|
1028
|
+
// Only show CORS/mixed-content warning for actual network failures (no status code)
|
|
1029
|
+
// AND when querying HTTP from HTTPS
|
|
951
1030
|
if (!error.status) {
|
|
952
|
-
// Only show this custom error if
|
|
953
1031
|
const shouldReferToHttp =
|
|
954
1032
|
new URL(tab.getEndpoint()).protocol === "http:" && window.location.protocol === "https:";
|
|
955
|
-
|
|
1033
|
+
|
|
1034
|
+
// Check if this looks like a network error (not just missing status)
|
|
1035
|
+
const isNetworkError =
|
|
1036
|
+
!error.text ||
|
|
1037
|
+
error.text.indexOf("Request has been terminated") >= 0 ||
|
|
1038
|
+
error.text.indexOf("Failed to fetch") >= 0 ||
|
|
1039
|
+
error.text.indexOf("NetworkError") >= 0 ||
|
|
1040
|
+
error.text.indexOf("Network request failed") >= 0;
|
|
1041
|
+
|
|
1042
|
+
if (shouldReferToHttp && isNetworkError) {
|
|
956
1043
|
const errorEl = document.createElement("div");
|
|
957
1044
|
const errorSpan = document.createElement("p");
|
|
958
1045
|
errorSpan.innerHTML = `You are trying to query an HTTP endpoint (<a href="${safeEndpoint(
|
|
@@ -961,14 +1048,7 @@ function getCorsErrorRenderer(tab: Tab) {
|
|
|
961
1048
|
tab.getEndpoint(),
|
|
962
1049
|
)}</a>) from an HTTP<strong>S</strong> website (<a href="${safeEndpoint(window.location.href)}">${safeEndpoint(
|
|
963
1050
|
window.location.href,
|
|
964
|
-
)}</a>).<br>This
|
|
965
|
-
if (tab.yasgui.config.nonSslDomain) {
|
|
966
|
-
const errorLink = document.createElement("p");
|
|
967
|
-
errorLink.innerHTML = `As a workaround, you can use the HTTP version of Yasgui instead: <a href="${tab.getShareableLink(
|
|
968
|
-
tab.yasgui.config.nonSslDomain,
|
|
969
|
-
)}" target="_blank">${tab.yasgui.config.nonSslDomain}</a>`;
|
|
970
|
-
errorSpan.appendChild(errorLink);
|
|
971
|
-
}
|
|
1051
|
+
)}</a>).<br>This can be blocked in modern browsers, see <a target="_blank" rel="noopener noreferrer" href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy</a>. See also <a href="https://yasgui-doc.matdata.eu/docs/user-guide#querying-local-endpoints">the YasGUI documentation</a> for possible workarounds.`;
|
|
972
1052
|
errorEl.appendChild(errorSpan);
|
|
973
1053
|
return errorEl;
|
|
974
1054
|
}
|