@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.
Files changed (38) hide show
  1. package/build/ts/src/ConfigExportImport.js +3 -0
  2. package/build/ts/src/ConfigExportImport.js.map +1 -1
  3. package/build/ts/src/OAuth2Utils.d.ts +18 -0
  4. package/build/ts/src/OAuth2Utils.js +214 -0
  5. package/build/ts/src/OAuth2Utils.js.map +1 -0
  6. package/build/ts/src/PersistentConfig.d.ts +3 -0
  7. package/build/ts/src/PersistentConfig.js +7 -0
  8. package/build/ts/src/PersistentConfig.js.map +1 -1
  9. package/build/ts/src/Tab.d.ts +1 -1
  10. package/build/ts/src/Tab.js +116 -85
  11. package/build/ts/src/Tab.js.map +1 -1
  12. package/build/ts/src/TabSettingsModal.d.ts +1 -0
  13. package/build/ts/src/TabSettingsModal.js +330 -27
  14. package/build/ts/src/TabSettingsModal.js.map +1 -1
  15. package/build/ts/src/defaults.js +1 -1
  16. package/build/ts/src/defaults.js.map +1 -1
  17. package/build/ts/src/index.d.ts +21 -6
  18. package/build/ts/src/index.js +7 -1
  19. package/build/ts/src/index.js.map +1 -1
  20. package/build/ts/src/version.d.ts +1 -1
  21. package/build/ts/src/version.js +1 -1
  22. package/build/yasgui.min.css +1 -1
  23. package/build/yasgui.min.css.map +3 -3
  24. package/build/yasgui.min.js +185 -157
  25. package/build/yasgui.min.js.map +4 -4
  26. package/package.json +3 -2
  27. package/src/ConfigExportImport.ts +3 -0
  28. package/src/OAuth2Utils.ts +315 -0
  29. package/src/PersistentConfig.ts +10 -0
  30. package/src/Tab.ts +191 -111
  31. package/src/TabSettingsModal.scss +70 -3
  32. package/src/TabSettingsModal.ts +400 -30
  33. package/src/defaults.ts +1 -1
  34. package/src/endpointSelect.scss +12 -0
  35. package/src/index.ts +42 -10
  36. package/src/tab.scss +1 -0
  37. package/src/themes.scss +1 -0
  38. 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
- if (endpointConfig.authentication.type === "basic") {
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
- username: endpointConfig.authentication.username,
496
- password: endpointConfig.authentication.password,
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 basicAuth is not already set
681
+ // Only inject endpoint-based auth if the corresponding auth type is not already set
566
682
  const endpointAuth = this.getAuthForCurrentEndpoint();
567
- if (endpointAuth && typeof processedReqConfig.basicAuth === "undefined") {
568
- processedReqConfig.basicAuth = endpointAuth;
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
- this.executeBackgroundQuery(constructQuery);
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
- // Get the request configuration
751
- const requestConfig = this.yasqe.config.requestConfig;
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
- const fetchOptions: RequestInit = {
776
- method: method,
777
- headers: {
778
- Accept: "text/turtle",
779
- ...headers,
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: error instanceof Error ? error.message : "Unknown 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
- if (shouldReferToHttp) {
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 is not allowed 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>.`;
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
  }