@matdata/yasgui 5.5.0 → 5.6.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasgui",
3
3
  "description": "Yet Another SPARQL GUI",
4
- "version": "5.5.0",
4
+ "version": "5.6.0",
5
5
  "main": "build/yasgui.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -305,6 +305,8 @@ export function parseFromTurtle(turtle: string): Partial<PersistedJson> {
305
305
  withCredentials: false,
306
306
  adjustQueryBeforeRequest: false,
307
307
  basicAuth: undefined,
308
+ bearerAuth: undefined,
309
+ apiKeyAuth: undefined,
308
310
  },
309
311
  yasr: {
310
312
  settings: {},
@@ -14,6 +14,7 @@ export interface PersistedJson {
14
14
  endpointConfigs?: EndpointConfig[]; // New endpoint-based storage with auth
15
15
  theme?: "light" | "dark";
16
16
  orientation?: "vertical" | "horizontal";
17
+ showSnippetsBar?: boolean;
17
18
  }
18
19
  function getDefaults(): PersistedJson {
19
20
  return {
@@ -166,6 +167,15 @@ export default class PersistentConfig {
166
167
  this.toStorage();
167
168
  }
168
169
 
170
+ public getShowSnippetsBar(): boolean | undefined {
171
+ return this.persistedJson.showSnippetsBar;
172
+ }
173
+
174
+ public setShowSnippetsBar(show: boolean) {
175
+ this.persistedJson.showSnippetsBar = show;
176
+ this.toStorage();
177
+ }
178
+
169
179
  // New endpoint configuration methods
170
180
  public getEndpointConfigs(): EndpointConfig[] {
171
181
  return this.persistedJson.endpointConfigs || [];
package/src/Tab.ts CHANGED
@@ -20,9 +20,11 @@ const VERTICAL_LAYOUT_ICON = `<svg viewBox="0 0 24 24" class="svgImg">
20
20
  <rect x="2" y="2" width="20" height="8" stroke="currentColor" stroke-width="2" fill="none"/>
21
21
  <rect x="2" y="12" width="20" height="10" stroke="currentColor" stroke-width="2" fill="none"/>
22
22
  </svg>`;
23
+
23
24
  export interface PersistedJsonYasr extends YasrPersistentConfig {
24
25
  responseSummary: Parser.ResponseSummary;
25
26
  }
27
+
26
28
  export interface PersistedJson {
27
29
  name: string;
28
30
  id: string;
@@ -37,6 +39,7 @@ export interface PersistedJson {
37
39
  requestConfig: YasguiRequestConfig;
38
40
  orientation?: "vertical" | "horizontal";
39
41
  }
42
+
40
43
  export interface Tab {
41
44
  on(event: string | symbol, listener: (...args: any[]) => void): this;
42
45
 
@@ -59,6 +62,7 @@ export interface Tab {
59
62
  on(event: "autocompletionClose", listener: (tab: Tab) => void): this;
60
63
  emit(event: "autocompletionClose", tab: Tab): boolean;
61
64
  }
65
+
62
66
  export class Tab extends EventEmitter {
63
67
  private persistentJson: PersistedJson;
64
68
  public yasgui: Yasgui;
@@ -73,6 +77,7 @@ export class Tab extends EventEmitter {
73
77
  private settingsModal?: TabSettingsModal;
74
78
  private currentOrientation: "vertical" | "horizontal";
75
79
  private orientationToggleButton?: HTMLButtonElement;
80
+
76
81
  constructor(yasgui: Yasgui, conf: PersistedJson) {
77
82
  super();
78
83
  if (!conf || conf.id === undefined) throw new Error("Expected a valid configuration to initialize tab with");
@@ -80,15 +85,19 @@ export class Tab extends EventEmitter {
80
85
  this.persistentJson = conf;
81
86
  this.currentOrientation = this.yasgui.config.orientation || "vertical";
82
87
  }
88
+
83
89
  public name() {
84
90
  return this.persistentJson.name;
85
91
  }
92
+
86
93
  public getPersistedJson() {
87
94
  return this.persistentJson;
88
95
  }
96
+
89
97
  public getId() {
90
98
  return this.persistentJson.id;
91
99
  }
100
+
92
101
  private draw() {
93
102
  if (this.rootEl) return; //aready drawn
94
103
  this.rootEl = document.createElement("div");
@@ -128,10 +137,12 @@ export class Tab extends EventEmitter {
128
137
  this.initYasr();
129
138
  this.yasgui._setPanel(this.persistentJson.id, this.rootEl);
130
139
  }
140
+
131
141
  public hide() {
132
142
  removeClass(this.rootEl, "active");
133
143
  this.detachKeyboardListeners();
134
144
  }
145
+
135
146
  public show() {
136
147
  this.draw();
137
148
  addClass(this.rootEl, "active");
@@ -200,9 +211,11 @@ export class Tab extends EventEmitter {
200
211
  private detachKeyboardListeners() {
201
212
  document.removeEventListener("keydown", this.handleKeyDown);
202
213
  }
214
+
203
215
  public select() {
204
216
  this.yasgui.selectTabId(this.persistentJson.id);
205
217
  }
218
+
206
219
  public close() {
207
220
  this.detachKeyboardListeners();
208
221
  if (this.yasqe) this.yasqe.abortQuery();
@@ -222,12 +235,14 @@ export class Tab extends EventEmitter {
222
235
  this.yasgui.tabElements.get(this.persistentJson.id).delete();
223
236
  delete this.yasgui._tabs[this.persistentJson.id];
224
237
  }
238
+
225
239
  public getQuery() {
226
240
  if (!this.yasqe) {
227
241
  throw new Error("Cannot get value from uninitialized editor");
228
242
  }
229
243
  return this.yasqe?.getValue();
230
244
  }
245
+
231
246
  public setQuery(query: string) {
232
247
  if (!this.yasqe) {
233
248
  throw new Error("Cannot set value for uninitialized editor");
@@ -237,9 +252,11 @@ export class Tab extends EventEmitter {
237
252
  this.emit("change", this, this.persistentJson);
238
253
  return this;
239
254
  }
255
+
240
256
  public getRequestConfig() {
241
257
  return this.persistentJson.requestConfig;
242
258
  }
259
+
243
260
  private initControlbar() {
244
261
  this.initOrientationToggle();
245
262
  this.initEndpointSelectField();
@@ -313,12 +330,15 @@ export class Tab extends EventEmitter {
313
330
  }
314
331
  }
315
332
  }
333
+
316
334
  public getYasqe() {
317
335
  return this.yasqe;
318
336
  }
337
+
319
338
  public getYasr() {
320
339
  return this.yasr;
321
340
  }
341
+
322
342
  private initTabSettingsMenu() {
323
343
  if (!this.controlBarEl) throw new Error("Need to initialize wrapper elements before drawing tab settings");
324
344
  this.settingsModal = new TabSettingsModal(this, this.controlBarEl);
@@ -397,26 +417,11 @@ export class Tab extends EventEmitter {
397
417
  });
398
418
  }
399
419
 
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
420
  public setEndpoint(endpoint: string, endpointHistory?: string[]) {
415
421
  if (endpoint) endpoint = endpoint.trim();
416
422
  if (endpointHistory && !eq(endpointHistory, this.yasgui.persistentConfig.getEndpointHistory())) {
417
423
  this.yasgui.emit("endpointHistoryChange", this.yasgui, endpointHistory);
418
424
  }
419
- this.checkEndpointForCors(endpoint); //little cost in checking this as we're caching the check results
420
425
 
421
426
  if (this.persistentJson.requestConfig.endpoint !== endpoint) {
422
427
  this.persistentJson.requestConfig.endpoint = endpoint;
@@ -433,9 +438,11 @@ export class Tab extends EventEmitter {
433
438
  }
434
439
  return this;
435
440
  }
441
+
436
442
  public getEndpoint(): string {
437
443
  return getAsValue(this.persistentJson.requestConfig.endpoint, this.yasgui);
438
444
  }
445
+
439
446
  /**
440
447
  * Updates the position of the Tab's contextmenu
441
448
  * Useful for when being scrolled
@@ -443,21 +450,26 @@ export class Tab extends EventEmitter {
443
450
  public updateContextMenu(): void {
444
451
  this.getTabListEl().redrawContextMenu();
445
452
  }
453
+
446
454
  public getShareableLink(baseURL?: string): string {
447
455
  return shareLink.createShareLink(baseURL || window.location.href, this);
448
456
  }
457
+
449
458
  public getShareObject() {
450
459
  return shareLink.createShareConfig(this);
451
460
  }
461
+
452
462
  private getTabListEl(): TabListEl {
453
463
  return this.yasgui.tabElements.get(this.persistentJson.id);
454
464
  }
465
+
455
466
  public setName(newName: string) {
456
467
  this.getTabListEl().rename(newName);
457
468
  this.persistentJson.name = newName;
458
469
  this.emit("change", this, this.persistentJson);
459
470
  return this;
460
471
  }
472
+
461
473
  public hasResults() {
462
474
  return !!this.yasr?.results;
463
475
  }
@@ -465,10 +477,12 @@ export class Tab extends EventEmitter {
465
477
  public getName() {
466
478
  return this.persistentJson.name;
467
479
  }
480
+
468
481
  public query(): Promise<any> {
469
482
  if (!this.yasqe) return Promise.reject(new Error("No yasqe editor initialized"));
470
483
  return this.yasqe.query();
471
484
  }
485
+
472
486
  public setRequestConfig(requestConfig: Partial<YasguiRequestConfig>) {
473
487
  this.persistentJson.requestConfig = {
474
488
  ...this.persistentJson.requestConfig,
@@ -490,10 +504,29 @@ export class Tab extends EventEmitter {
490
504
  if (!endpointConfig || !endpointConfig.authentication) return undefined;
491
505
 
492
506
  // Convert endpoint auth to requestConfig format
493
- if (endpointConfig.authentication.type === "basic") {
507
+ const auth = endpointConfig.authentication;
508
+ if (auth.type === "basic") {
509
+ return {
510
+ type: "basic" as const,
511
+ config: {
512
+ username: auth.username,
513
+ password: auth.password,
514
+ },
515
+ };
516
+ } else if (auth.type === "bearer") {
494
517
  return {
495
- username: endpointConfig.authentication.username,
496
- password: endpointConfig.authentication.password,
518
+ type: "bearer" as const,
519
+ config: {
520
+ token: auth.token,
521
+ },
522
+ };
523
+ } else if (auth.type === "apiKey") {
524
+ return {
525
+ type: "apiKey" as const,
526
+ config: {
527
+ headerName: auth.headerName,
528
+ apiKey: auth.apiKey,
529
+ },
497
530
  };
498
531
  }
499
532
 
@@ -539,6 +572,8 @@ export class Tab extends EventEmitter {
539
572
  persistenceId: null, //yasgui handles persistent storing
540
573
  consumeShareLink: null, //not handled by this tab, but by parent yasgui instance
541
574
  createShareableLink: () => this.getShareableLink(),
575
+ // Use global showSnippetsBar setting if it exists
576
+ showSnippetsBar: this.yasgui.config.showSnippetsBar !== false,
542
577
  requestConfig: () => {
543
578
  const processedReqConfig: YasguiRequestConfig = {
544
579
  //setting defaults
@@ -562,24 +597,18 @@ export class Tab extends EventEmitter {
562
597
  };
563
598
 
564
599
  // Inject authentication from endpoint-based storage
565
- // Only inject endpoint-based auth if basicAuth is not already set
600
+ // Only inject endpoint-based auth if the corresponding auth type is not already set
566
601
  const endpointAuth = this.getAuthForCurrentEndpoint();
567
- if (endpointAuth && typeof processedReqConfig.basicAuth === "undefined") {
568
- processedReqConfig.basicAuth = endpointAuth;
602
+ if (endpointAuth) {
603
+ if (endpointAuth.type === "basic" && typeof processedReqConfig.basicAuth === "undefined") {
604
+ processedReqConfig.basicAuth = endpointAuth.config;
605
+ } else if (endpointAuth.type === "bearer" && typeof processedReqConfig.bearerAuth === "undefined") {
606
+ processedReqConfig.bearerAuth = endpointAuth.config;
607
+ } else if (endpointAuth.type === "apiKey" && typeof processedReqConfig.apiKeyAuth === "undefined") {
608
+ processedReqConfig.apiKeyAuth = endpointAuth.config;
609
+ }
569
610
  }
570
611
 
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
612
  return processedReqConfig as PlainRequestConfig;
584
613
  },
585
614
  };
@@ -608,6 +637,7 @@ export class Tab extends EventEmitter {
608
637
  // Add Ctrl+Click handler for URIs
609
638
  this.attachYasqeMouseHandler();
610
639
  }
640
+
611
641
  private destroyYasqe() {
612
642
  // As Yasqe extends of CM instead of eventEmitter, it doesn't expose the removeAllListeners function, so we should unregister all events manually
613
643
  this.yasqe?.off("blur", this.handleYasqeBlur);
@@ -622,12 +652,14 @@ export class Tab extends EventEmitter {
622
652
  this.yasqe?.destroy();
623
653
  this.yasqe = undefined;
624
654
  }
655
+
625
656
  handleYasqeBlur = (yasqe: Yasqe) => {
626
657
  this.persistentJson.yasqe.value = yasqe.getValue();
627
658
  // Capture prefixes from query if auto-capture is enabled
628
659
  this.settingsModal?.capturePrefixesFromQuery();
629
660
  this.emit("change", this, this.persistentJson);
630
661
  };
662
+
631
663
  handleYasqeQuery = (yasqe: Yasqe) => {
632
664
  //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
665
  if (yasqe.getValue() !== this.persistentJson.yasqe.value) {
@@ -636,6 +668,7 @@ export class Tab extends EventEmitter {
636
668
  }
637
669
  this.emit("query", this);
638
670
  };
671
+
639
672
  handleYasqeQueryAbort = () => {
640
673
  this.emit("queryAbort", this);
641
674
  // Hide loading indicator in Yasr
@@ -643,6 +676,7 @@ export class Tab extends EventEmitter {
643
676
  this.yasr.hideLoading();
644
677
  }
645
678
  };
679
+
646
680
  handleYasqeQueryBefore = () => {
647
681
  this.emit("queryBefore", this);
648
682
  // Show loading indicator in Yasr
@@ -650,16 +684,20 @@ export class Tab extends EventEmitter {
650
684
  this.yasr.showLoading();
651
685
  }
652
686
  };
687
+
653
688
  handleYasqeResize = (_yasqe: Yasqe, newSize: string) => {
654
689
  this.persistentJson.yasqe.editorHeight = newSize;
655
690
  this.emit("change", this, this.persistentJson);
656
691
  };
692
+
657
693
  handleAutocompletionShown = (_yasqe: Yasqe, widget: string) => {
658
694
  this.emit("autocompletionShown", this, widget);
659
695
  };
696
+
660
697
  handleAutocompletionClose = (_yasqe: Yasqe) => {
661
698
  this.emit("autocompletionClose", this);
662
699
  };
700
+
663
701
  handleQueryResponse = (_yasqe: Yasqe, response: any, duration: number) => {
664
702
  this.emit("queryResponse", this);
665
703
  if (!this.yasr) throw new Error("Resultset visualizer not initialized. Cannot draw results");
@@ -736,7 +774,8 @@ WHERE {
736
774
  } LIMIT 1000`;
737
775
 
738
776
  // Execute query in background without changing editor content
739
- this.executeBackgroundQuery(constructQuery);
777
+ // Note: void operator is intentional - errors are handled in the catch block of executeBackgroundQuery
778
+ void this.executeBackgroundQuery(constructQuery);
740
779
  };
741
780
 
742
781
  private async executeBackgroundQuery(query: string) {
@@ -747,75 +786,21 @@ WHERE {
747
786
  this.yasr.showLoading();
748
787
  this.emit("queryBefore", this);
749
788
 
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
- }
789
+ // Track query execution time
790
+ const startTime = Date.now();
774
791
 
775
- const fetchOptions: RequestInit = {
776
- method: method,
777
- headers: {
778
- Accept: "text/turtle",
779
- ...headers,
792
+ // Use yasqe's executeQuery with custom query and accept header
793
+ const queryResponse = await Yasqe.Sparql.executeQuery(
794
+ this.yasqe,
795
+ undefined, // Use default config
796
+ {
797
+ customQuery: query,
798
+ customAccept: "text/turtle",
780
799
  },
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
- }
800
+ );
797
801
 
798
- const startTime = Date.now();
799
- const response = await fetch(url, fetchOptions);
800
802
  const duration = Date.now() - startTime;
801
803
 
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
804
  // Set the response in Yasr
820
805
  this.yasr.setResponse(queryResponse, duration);
821
806
 
@@ -831,10 +816,17 @@ WHERE {
831
816
  console.error("Background query failed:", error);
832
817
  if (this.yasr) {
833
818
  this.yasr.hideLoading();
834
- // Set error response
819
+ // Set error response with detailed HTTP status if available
820
+ const errorObj: any = error;
821
+ let errorText = error instanceof Error ? error.message : String(error);
822
+
835
823
  this.yasr.setResponse(
836
824
  {
837
- error: error instanceof Error ? error.message : "Unknown error",
825
+ error: {
826
+ status: errorObj.status,
827
+ statusText: errorObj.statusText || (error instanceof Error ? error.name : undefined),
828
+ text: errorText,
829
+ },
838
830
  },
839
831
  0,
840
832
  );
@@ -910,6 +902,7 @@ WHERE {
910
902
  this.emit("change", this, this.persistentJson);
911
903
  });
912
904
  }
905
+
913
906
  destroy() {
914
907
  this.removeAllListeners();
915
908
  this.settingsModal?.destroy();
@@ -919,6 +912,7 @@ WHERE {
919
912
  this.yasr = undefined;
920
913
  this.destroyYasqe();
921
914
  }
915
+
922
916
  public static getDefaults(yasgui?: Yasgui): PersistedJson {
923
917
  return {
924
918
  yasqe: {
@@ -948,11 +942,21 @@ const safeEndpoint = (endpoint: string): string => {
948
942
 
949
943
  function getCorsErrorRenderer(tab: Tab) {
950
944
  return async (error: Parser.ErrorSummary): Promise<HTMLElement | undefined> => {
945
+ // Only show CORS/mixed-content warning for actual network failures (no status code)
946
+ // AND when querying HTTP from HTTPS
951
947
  if (!error.status) {
952
- // Only show this custom error if
953
948
  const shouldReferToHttp =
954
949
  new URL(tab.getEndpoint()).protocol === "http:" && window.location.protocol === "https:";
955
- if (shouldReferToHttp) {
950
+
951
+ // Check if this looks like a network error (not just missing status)
952
+ const isNetworkError =
953
+ !error.text ||
954
+ error.text.indexOf("Request has been terminated") >= 0 ||
955
+ error.text.indexOf("Failed to fetch") >= 0 ||
956
+ error.text.indexOf("NetworkError") >= 0 ||
957
+ error.text.indexOf("Network request failed") >= 0;
958
+
959
+ if (shouldReferToHttp && isNetworkError) {
956
960
  const errorEl = document.createElement("div");
957
961
  const errorSpan = document.createElement("p");
958
962
  errorSpan.innerHTML = `You are trying to query an HTTP endpoint (<a href="${safeEndpoint(
@@ -961,14 +965,7 @@ function getCorsErrorRenderer(tab: Tab) {
961
965
  tab.getEndpoint(),
962
966
  )}</a>) from an HTTP<strong>S</strong> website (<a href="${safeEndpoint(window.location.href)}">${safeEndpoint(
963
967
  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
- }
968
+ )}</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
969
  errorEl.appendChild(errorSpan);
973
970
  return errorEl;
974
971
  }