@matdata/yasgui 5.10.0 → 5.11.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/README.md +6 -2
- package/build/ts/src/PersistentConfig.d.ts +10 -0
- package/build/ts/src/PersistentConfig.js +40 -0
- package/build/ts/src/PersistentConfig.js.map +1 -1
- package/build/ts/src/Tab.d.ts +17 -0
- package/build/ts/src/Tab.js +372 -15
- package/build/ts/src/Tab.js.map +1 -1
- package/build/ts/src/TabContextMenu.d.ts +1 -0
- package/build/ts/src/TabContextMenu.js +17 -0
- package/build/ts/src/TabContextMenu.js.map +1 -1
- package/build/ts/src/TabElements.d.ts +3 -0
- package/build/ts/src/TabElements.js +97 -28
- package/build/ts/src/TabElements.js.map +1 -1
- package/build/ts/src/TabSettingsModal.d.ts +2 -0
- package/build/ts/src/TabSettingsModal.js +44 -19
- package/build/ts/src/TabSettingsModal.js.map +1 -1
- package/build/ts/src/index.d.ts +3 -0
- package/build/ts/src/index.js +4 -0
- package/build/ts/src/index.js.map +1 -1
- package/build/ts/src/queryManagement/QueryBrowser.d.ts +64 -0
- package/build/ts/src/queryManagement/QueryBrowser.js +914 -0
- package/build/ts/src/queryManagement/QueryBrowser.js.map +1 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.d.ts +55 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js +451 -0
- package/build/ts/src/queryManagement/SaveManagedQueryModal.js.map +1 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.d.ts +16 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js +452 -0
- package/build/ts/src/queryManagement/WorkspaceSettingsForm.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.d.ts +19 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js +77 -0
- package/build/ts/src/queryManagement/backends/BaseGitProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js +269 -0
- package/build/ts/src/queryManagement/backends/BitbucketProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.d.ts +26 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js +93 -0
- package/build/ts/src/queryManagement/backends/GitWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js +244 -0
- package/build/ts/src/queryManagement/backends/GiteaProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.d.ts +14 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js +252 -0
- package/build/ts/src/queryManagement/backends/GithubProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.d.ts +16 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js +246 -0
- package/build/ts/src/queryManagement/backends/GitlabProviderClient.js.map +1 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.d.ts +21 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js +175 -0
- package/build/ts/src/queryManagement/backends/InMemoryWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.d.ts +28 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js +687 -0
- package/build/ts/src/queryManagement/backends/SparqlWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.d.ts +15 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js +2 -0
- package/build/ts/src/queryManagement/backends/WorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/errors.d.ts +7 -0
- package/build/ts/src/queryManagement/backends/errors.js +18 -0
- package/build/ts/src/queryManagement/backends/errors.js.map +1 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.d.ts +8 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js +114 -0
- package/build/ts/src/queryManagement/backends/getWorkspaceBackend.js.map +1 -0
- package/build/ts/src/queryManagement/backends/gitRemote.d.ts +5 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js +40 -0
- package/build/ts/src/queryManagement/backends/gitRemote.js.map +1 -0
- package/build/ts/src/queryManagement/browserFilter.d.ts +2 -0
- package/build/ts/src/queryManagement/browserFilter.js +7 -0
- package/build/ts/src/queryManagement/browserFilter.js.map +1 -0
- package/build/ts/src/queryManagement/index.d.ts +13 -0
- package/build/ts/src/queryManagement/index.js +14 -0
- package/build/ts/src/queryManagement/index.js.map +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.d.ts +1 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js +10 -0
- package/build/ts/src/queryManagement/normalizeQueryFilename.js.map +1 -0
- package/build/ts/src/queryManagement/openManagedQuery.d.ts +15 -0
- package/build/ts/src/queryManagement/openManagedQuery.js +27 -0
- package/build/ts/src/queryManagement/openManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/saveManagedQuery.d.ts +20 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js +109 -0
- package/build/ts/src/queryManagement/saveManagedQuery.js.map +1 -0
- package/build/ts/src/queryManagement/textHash.d.ts +2 -0
- package/build/ts/src/queryManagement/textHash.js +13 -0
- package/build/ts/src/queryManagement/textHash.js.map +1 -0
- package/build/ts/src/queryManagement/types.d.ts +76 -0
- package/build/ts/src/queryManagement/types.js +2 -0
- package/build/ts/src/queryManagement/types.js.map +1 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.d.ts +6 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.js +33 -0
- package/build/ts/src/queryManagement/validateWorkspaceConfig.js.map +1 -0
- package/build/ts/src/version.d.ts +1 -1
- package/build/ts/src/version.js +1 -1
- package/build/yasgui.min.css +10 -1
- package/build/yasgui.min.css.map +3 -3
- package/build/yasgui.min.js +398 -172
- package/build/yasgui.min.js.map +4 -4
- package/package.json +1 -1
- package/src/PersistentConfig.ts +61 -0
- package/src/Tab.ts +431 -20
- package/src/TabContextMenu.ts +10 -0
- package/src/TabElements.scss +46 -7
- package/src/TabElements.ts +95 -27
- package/src/TabSettingsModal.scss +102 -5
- package/src/TabSettingsModal.ts +48 -25
- package/src/endpointSelect.scss +2 -3
- package/src/index.scss +4 -0
- package/src/index.ts +7 -0
- package/src/queryManagement/QueryBrowser.scss +418 -0
- package/src/queryManagement/QueryBrowser.ts +1079 -0
- package/src/queryManagement/SaveManagedQueryModal.scss +245 -0
- package/src/queryManagement/SaveManagedQueryModal.ts +554 -0
- package/src/queryManagement/WorkspaceSettingsForm.ts +546 -0
- package/src/queryManagement/backends/BaseGitProviderClient.ts +124 -0
- package/src/queryManagement/backends/BitbucketProviderClient.ts +403 -0
- package/src/queryManagement/backends/GitWorkspaceBackend.ts +96 -0
- package/src/queryManagement/backends/GiteaProviderClient.ts +316 -0
- package/src/queryManagement/backends/GithubProviderClient.ts +319 -0
- package/src/queryManagement/backends/GitlabProviderClient.ts +327 -0
- package/src/queryManagement/backends/InMemoryWorkspaceBackend.ts +175 -0
- package/src/queryManagement/backends/SparqlWorkspaceBackend.ts +729 -0
- package/src/queryManagement/backends/WorkspaceBackend.ts +41 -0
- package/src/queryManagement/backends/errors.ts +28 -0
- package/src/queryManagement/backends/getWorkspaceBackend.ts +132 -0
- package/src/queryManagement/backends/gitRemote.ts +54 -0
- package/src/queryManagement/browserFilter.ts +8 -0
- package/src/queryManagement/index.ts +15 -0
- package/src/queryManagement/normalizeQueryFilename.ts +8 -0
- package/src/queryManagement/openManagedQuery.ts +31 -0
- package/src/queryManagement/saveManagedQuery.ts +135 -0
- package/src/queryManagement/textHash.ts +15 -0
- package/src/queryManagement/types.ts +85 -0
- package/src/queryManagement/validateWorkspaceConfig.ts +40 -0
- package/src/tab.scss +4 -14
- package/src/themes.scss +14 -23
- package/src/version.ts +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BackendType, FolderEntry, ReadResult, VersionInfo, WriteQueryOptions } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceBackend {
|
|
4
|
+
readonly type: BackendType;
|
|
5
|
+
|
|
6
|
+
validateAccess(): Promise<void>;
|
|
7
|
+
|
|
8
|
+
listFolder(folderId?: string): Promise<FolderEntry[]>;
|
|
9
|
+
|
|
10
|
+
searchByName?(query: string): Promise<FolderEntry[]>;
|
|
11
|
+
|
|
12
|
+
readQuery(queryId: string): Promise<ReadResult>;
|
|
13
|
+
|
|
14
|
+
writeQuery(queryId: string, queryText: string, options?: WriteQueryOptions): Promise<void>;
|
|
15
|
+
|
|
16
|
+
listVersions(queryId: string): Promise<VersionInfo[]>;
|
|
17
|
+
|
|
18
|
+
readVersion(queryId: string, versionId: string): Promise<ReadResult>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Optional: Rename a query (typically renaming its label, not its ID).
|
|
22
|
+
* Implementations may not support this (e.g., some Git provider clients).
|
|
23
|
+
*/
|
|
24
|
+
renameQuery?(queryId: string, newLabel: string): Promise<void>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Optional: Delete a query and its version history.
|
|
28
|
+
* Implementations may not support this (e.g., some Git provider clients).
|
|
29
|
+
*/
|
|
30
|
+
deleteQuery?(queryId: string): Promise<void>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional: Rename a folder (typically renaming its label, not its ID).
|
|
34
|
+
*/
|
|
35
|
+
renameFolder?(folderId: string, newLabel: string): Promise<void>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional: Delete a folder and everything inside it (recursive).
|
|
39
|
+
*/
|
|
40
|
+
deleteFolder?(folderId: string): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type WorkspaceBackendErrorCode =
|
|
2
|
+
| "AUTH_FAILED"
|
|
3
|
+
| "FORBIDDEN"
|
|
4
|
+
| "NOT_FOUND"
|
|
5
|
+
| "NETWORK_ERROR"
|
|
6
|
+
| "RATE_LIMITED"
|
|
7
|
+
| "CONFLICT"
|
|
8
|
+
| "UNKNOWN";
|
|
9
|
+
|
|
10
|
+
export class WorkspaceBackendError extends Error {
|
|
11
|
+
public readonly code: WorkspaceBackendErrorCode;
|
|
12
|
+
|
|
13
|
+
constructor(code: WorkspaceBackendErrorCode, message?: string) {
|
|
14
|
+
super(message || code);
|
|
15
|
+
this.name = "WorkspaceBackendError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isWorkspaceBackendError(error: unknown): error is WorkspaceBackendError {
|
|
21
|
+
return typeof error === "object" && error !== null && (error as any).name === "WorkspaceBackendError";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function asWorkspaceBackendError(error: unknown): WorkspaceBackendError {
|
|
25
|
+
if (isWorkspaceBackendError(error)) return error;
|
|
26
|
+
if (error instanceof Error) return new WorkspaceBackendError("UNKNOWN", error.message);
|
|
27
|
+
return new WorkspaceBackendError("UNKNOWN", String(error));
|
|
28
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { WorkspaceConfig } from "../types";
|
|
2
|
+
import type { WorkspaceBackend } from "./WorkspaceBackend";
|
|
3
|
+
import GitWorkspaceBackend from "./GitWorkspaceBackend";
|
|
4
|
+
import SparqlWorkspaceBackend from "./SparqlWorkspaceBackend";
|
|
5
|
+
import { GithubProviderClient } from "./GithubProviderClient";
|
|
6
|
+
import { GitlabProviderClient } from "./GitlabProviderClient";
|
|
7
|
+
import { BitbucketProviderClient } from "./BitbucketProviderClient";
|
|
8
|
+
import { GiteaProviderClient } from "./GiteaProviderClient";
|
|
9
|
+
import type PersistentConfig from "../../PersistentConfig";
|
|
10
|
+
import type { EndpointConfig } from "../../index";
|
|
11
|
+
import * as OAuth2Utils from "../../OAuth2Utils";
|
|
12
|
+
|
|
13
|
+
const registeredBackends = new Map<string, WorkspaceBackend>();
|
|
14
|
+
|
|
15
|
+
export function registerWorkspaceBackend(workspaceId: string, backend: WorkspaceBackend) {
|
|
16
|
+
registeredBackends.set(workspaceId, backend);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function unregisterWorkspaceBackend(workspaceId: string) {
|
|
20
|
+
registeredBackends.delete(workspaceId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function base64Encode(value: string): string {
|
|
24
|
+
// Browser: btoa; Node: Buffer
|
|
25
|
+
if (typeof (globalThis as any).btoa === "function") return (globalThis as any).btoa(value);
|
|
26
|
+
const buf = (globalThis as any).Buffer;
|
|
27
|
+
if (buf && typeof buf.from === "function") return buf.from(value, "utf8").toString("base64");
|
|
28
|
+
throw new Error("Base64 encoding not available in this environment");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveSparqlAuthHeaders(persistentConfig: PersistentConfig | undefined, endpoint: string) {
|
|
32
|
+
if (!persistentConfig) return undefined;
|
|
33
|
+
const cfg: EndpointConfig | undefined = persistentConfig.getEndpointConfig(endpoint);
|
|
34
|
+
const auth = cfg?.authentication;
|
|
35
|
+
if (!auth) return undefined;
|
|
36
|
+
|
|
37
|
+
if (auth.type === "basic") {
|
|
38
|
+
const token = base64Encode(`${auth.username}:${auth.password}`);
|
|
39
|
+
return { Authorization: `Basic ${token}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (auth.type === "bearer") {
|
|
43
|
+
return { Authorization: `Bearer ${auth.token}` };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (auth.type === "apiKey") {
|
|
47
|
+
return { [auth.headerName]: auth.apiKey };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (auth.type === "oauth2") {
|
|
51
|
+
const token = auth.accessToken || auth.idToken;
|
|
52
|
+
if (!token) return undefined;
|
|
53
|
+
return { Authorization: `Bearer ${token}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function resolveSparqlAuthHeadersWithRefresh(persistentConfig: PersistentConfig, endpoint: string) {
|
|
60
|
+
const cfg: EndpointConfig | undefined = persistentConfig.getEndpointConfig(endpoint);
|
|
61
|
+
const auth = cfg?.authentication;
|
|
62
|
+
if (!auth) return undefined;
|
|
63
|
+
|
|
64
|
+
if (auth.type === "oauth2") {
|
|
65
|
+
if (OAuth2Utils.isTokenExpired(auth.tokenExpiry)) {
|
|
66
|
+
if (auth.refreshToken) {
|
|
67
|
+
try {
|
|
68
|
+
const tokenResponse = await OAuth2Utils.refreshOAuth2Token(
|
|
69
|
+
{
|
|
70
|
+
clientId: auth.clientId,
|
|
71
|
+
tokenEndpoint: auth.tokenEndpoint,
|
|
72
|
+
},
|
|
73
|
+
auth.refreshToken,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const tokenExpiry = OAuth2Utils.calculateTokenExpiry(tokenResponse.expires_in);
|
|
77
|
+
persistentConfig.addOrUpdateEndpoint(endpoint, {
|
|
78
|
+
authentication: {
|
|
79
|
+
...auth,
|
|
80
|
+
accessToken: tokenResponse.access_token,
|
|
81
|
+
idToken: tokenResponse.id_token,
|
|
82
|
+
refreshToken: tokenResponse.refresh_token || auth.refreshToken,
|
|
83
|
+
tokenExpiry,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error("Failed to refresh OAuth2 token for endpoint", endpoint, e);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const updated = persistentConfig.getEndpointConfig(endpoint)?.authentication;
|
|
93
|
+
if (updated && updated.type === "oauth2") {
|
|
94
|
+
const token = updated.accessToken || updated.idToken;
|
|
95
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return resolveSparqlAuthHeaders(persistentConfig, endpoint);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getWorkspaceBackend(
|
|
105
|
+
config: WorkspaceConfig,
|
|
106
|
+
options?: {
|
|
107
|
+
persistentConfig?: PersistentConfig;
|
|
108
|
+
},
|
|
109
|
+
): WorkspaceBackend {
|
|
110
|
+
const registered = registeredBackends.get(config.id);
|
|
111
|
+
if (registered) return registered;
|
|
112
|
+
if (config.type === "git") {
|
|
113
|
+
const client =
|
|
114
|
+
(GithubProviderClient.canHandle(config) && new GithubProviderClient()) ||
|
|
115
|
+
(GitlabProviderClient.canHandle(config) && new GitlabProviderClient()) ||
|
|
116
|
+
(BitbucketProviderClient.canHandle(config) && new BitbucketProviderClient()) ||
|
|
117
|
+
(GiteaProviderClient.canHandle(config) && new GiteaProviderClient()) ||
|
|
118
|
+
undefined;
|
|
119
|
+
|
|
120
|
+
return new GitWorkspaceBackend(config, client);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const persistentConfig = options?.persistentConfig;
|
|
124
|
+
if (!persistentConfig) {
|
|
125
|
+
const authHeaders = resolveSparqlAuthHeaders(undefined, config.endpoint);
|
|
126
|
+
return new SparqlWorkspaceBackend(config, { authHeaders });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return new SparqlWorkspaceBackend(config, {
|
|
130
|
+
getAuthHeaders: () => resolveSparqlAuthHeadersWithRefresh(persistentConfig, config.endpoint),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type ParsedGitRemote = {
|
|
2
|
+
host: string;
|
|
3
|
+
/** Repository path without leading slash, without trailing .git */
|
|
4
|
+
repoPath: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function stripDotGit(path: string): string {
|
|
8
|
+
return path.replace(/\.git$/i, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function stripLeadingSlash(path: string): string {
|
|
12
|
+
return path.replace(/^\/+/, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stripTrailingSlash(path: string): string {
|
|
16
|
+
return path.replace(/\/+$/, "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseHttpLike(remoteUrl: string): ParsedGitRemote {
|
|
20
|
+
const url = new URL(remoteUrl);
|
|
21
|
+
const host = url.hostname.toLowerCase();
|
|
22
|
+
const repoPath = stripDotGit(stripTrailingSlash(stripLeadingSlash(url.pathname)));
|
|
23
|
+
if (!repoPath) throw new Error("Invalid git remote URL (missing path)");
|
|
24
|
+
return { host, repoPath };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseScpLike(remoteUrl: string): ParsedGitRemote {
|
|
28
|
+
// e.g. git@github.com:owner/repo.git
|
|
29
|
+
const m = /^([^@]+)@([^:]+):(.+)$/.exec(remoteUrl);
|
|
30
|
+
if (!m || !m[2] || !m[3]) throw new Error("Invalid SCP-like git remote URL");
|
|
31
|
+
const host = String(m[2]).toLowerCase();
|
|
32
|
+
const repoPath = stripDotGit(stripTrailingSlash(stripLeadingSlash(String(m[3]))));
|
|
33
|
+
if (!repoPath) throw new Error("Invalid git remote URL (missing path)");
|
|
34
|
+
return { host, repoPath };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseGitRemote(remoteUrl: string): ParsedGitRemote {
|
|
38
|
+
const trimmed = remoteUrl.trim();
|
|
39
|
+
if (!trimmed) throw new Error("Remote URL is empty");
|
|
40
|
+
|
|
41
|
+
// Handle common git remote formats.
|
|
42
|
+
if (/^(https?|ssh):\/\//i.test(trimmed)) {
|
|
43
|
+
// For ssh:// URLs, URL parsing works and pathname carries the repo path.
|
|
44
|
+
return parseHttpLike(trimmed);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// SCP-like syntax: git@host:org/repo.git
|
|
48
|
+
if (/^[^@]+@[^:]+:.+/.test(trimmed)) {
|
|
49
|
+
return parseScpLike(trimmed);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: attempt URL parse (may throw)
|
|
53
|
+
return parseHttpLike(trimmed);
|
|
54
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FolderEntry } from "./types";
|
|
2
|
+
|
|
3
|
+
export function filterFolderEntriesByName(entries: FolderEntry[], query: string): FolderEntry[] {
|
|
4
|
+
const q = query.trim().toLowerCase();
|
|
5
|
+
if (!q) return entries;
|
|
6
|
+
|
|
7
|
+
return entries.filter((e) => e.label.toLowerCase().includes(q));
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
|
|
3
|
+
export * from "./normalizeQueryFilename";
|
|
4
|
+
export * from "./saveManagedQuery";
|
|
5
|
+
export * from "./validateWorkspaceConfig";
|
|
6
|
+
export * from "./textHash";
|
|
7
|
+
export * from "./browserFilter";
|
|
8
|
+
export * from "./openManagedQuery";
|
|
9
|
+
export * from "./backends/WorkspaceBackend";
|
|
10
|
+
export * from "./backends/errors";
|
|
11
|
+
export * from "./backends/getWorkspaceBackend";
|
|
12
|
+
|
|
13
|
+
export { default as InMemoryWorkspaceBackend } from "./backends/InMemoryWorkspaceBackend";
|
|
14
|
+
export { default as GitWorkspaceBackend } from "./backends/GitWorkspaceBackend";
|
|
15
|
+
export { default as SparqlWorkspaceBackend } from "./backends/SparqlWorkspaceBackend";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BackendType, ReadResult } from "./types";
|
|
2
|
+
import type { WorkspaceBackend } from "./backends/WorkspaceBackend";
|
|
3
|
+
|
|
4
|
+
export function getEndpointToAutoSwitch(backendType: BackendType, readResult: ReadResult): string | undefined {
|
|
5
|
+
if (backendType === "git") return undefined;
|
|
6
|
+
return readResult.associatedEndpoint;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type NewTabLike = {
|
|
10
|
+
setQuery(queryText: string): void;
|
|
11
|
+
setEndpoint(endpoint: string): void;
|
|
12
|
+
setName(name: string): void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function openManagedQuery(options: {
|
|
16
|
+
backend: WorkspaceBackend;
|
|
17
|
+
queryId: string;
|
|
18
|
+
queryLabel?: string;
|
|
19
|
+
createNewTab: () => NewTabLike;
|
|
20
|
+
}): Promise<void> {
|
|
21
|
+
const read = await options.backend.readQuery(options.queryId);
|
|
22
|
+
|
|
23
|
+
const newTab = options.createNewTab();
|
|
24
|
+
|
|
25
|
+
if (options.queryLabel) newTab.setName(options.queryLabel);
|
|
26
|
+
|
|
27
|
+
const endpoint = getEndpointToAutoSwitch(options.backend.type, read);
|
|
28
|
+
if (endpoint) newTab.setEndpoint(endpoint);
|
|
29
|
+
|
|
30
|
+
newTab.setQuery(read.queryText);
|
|
31
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { WorkspaceBackend } from "./backends/WorkspaceBackend";
|
|
2
|
+
import type { BackendType, ManagedTabMetadata, VersionRef } from "./types";
|
|
3
|
+
import { asWorkspaceBackendError } from "./backends/errors";
|
|
4
|
+
import { hashQueryText } from "./textHash";
|
|
5
|
+
import { normalizeQueryFilename } from "./normalizeQueryFilename";
|
|
6
|
+
|
|
7
|
+
export interface SaveManagedQueryInput {
|
|
8
|
+
backend: WorkspaceBackend;
|
|
9
|
+
backendType: BackendType;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
/** Required for SPARQL workspaces to mint immutable query/version IRIs. */
|
|
12
|
+
workspaceIri?: string;
|
|
13
|
+
queryText: string;
|
|
14
|
+
folderPath?: string;
|
|
15
|
+
/** Tab/query label (used as SPARQL rdfs:label). */
|
|
16
|
+
name?: string;
|
|
17
|
+
filename: string;
|
|
18
|
+
/** Optional SPARQL query execution endpoint to store on the version (sparql backends only). */
|
|
19
|
+
associatedEndpoint?: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
expectedVersionTag?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SaveManagedQueryResult {
|
|
25
|
+
queryId: string;
|
|
26
|
+
managedMetadata: ManagedTabMetadata;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeFolderPath(folderPath: string | undefined): string {
|
|
30
|
+
if (!folderPath) return "";
|
|
31
|
+
const trimmed = folderPath.trim();
|
|
32
|
+
if (!trimmed) return "";
|
|
33
|
+
return trimmed.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildQueryId(folderPath: string | undefined, filename: string): string {
|
|
37
|
+
const normalizedFolder = normalizeFolderPath(folderPath);
|
|
38
|
+
const normalizedFilename = normalizeQueryFilename(filename);
|
|
39
|
+
return normalizedFolder ? `${normalizedFolder}/${normalizedFilename}` : normalizedFilename;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uuidV4(): string {
|
|
43
|
+
const cryptoObj = (globalThis as any).crypto as Crypto | undefined;
|
|
44
|
+
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
|
|
45
|
+
// RFC4122 v4 fallback (best-effort)
|
|
46
|
+
const rnds = new Uint8Array(16);
|
|
47
|
+
if (cryptoObj?.getRandomValues) {
|
|
48
|
+
cryptoObj.getRandomValues(rnds);
|
|
49
|
+
} else {
|
|
50
|
+
for (let i = 0; i < rnds.length; i++) rnds[i] = Math.floor(Math.random() * 256);
|
|
51
|
+
}
|
|
52
|
+
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
|
53
|
+
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
|
54
|
+
const hex = Array.from(rnds, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mintSparqlManagedQueryIri(workspaceIri: string): string {
|
|
59
|
+
return `${workspaceIri.trim()}_mq_${uuidV4()}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function versionRefFromVersionTag(backendType: BackendType, versionTag: string | undefined): VersionRef | undefined {
|
|
63
|
+
if (!versionTag) return undefined;
|
|
64
|
+
if (backendType === "git") return { commitSha: versionTag };
|
|
65
|
+
return { managedQueryVersionIri: versionTag };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function saveManagedQuery(input: SaveManagedQueryInput): Promise<SaveManagedQueryResult> {
|
|
69
|
+
const folderId = normalizeFolderPath(input.folderPath);
|
|
70
|
+
const queryId = input.backendType === "git" ? buildQueryId(input.folderPath, input.filename) : "";
|
|
71
|
+
|
|
72
|
+
if (input.backendType === "sparql" && !input.workspaceIri?.trim()) {
|
|
73
|
+
throw new Error("workspaceIri is required for SPARQL workspaces");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const label = (input.name || "").trim();
|
|
77
|
+
if (input.backendType === "sparql" && !label) {
|
|
78
|
+
throw new Error("name is required for SPARQL workspaces");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const resolvedQueryId = await (async () => {
|
|
82
|
+
if (input.backendType !== "sparql") return queryId;
|
|
83
|
+
|
|
84
|
+
// Overwrite behavior: if a query with the same label already exists in this folder,
|
|
85
|
+
// write a new version to that managed query instead of creating a duplicate.
|
|
86
|
+
try {
|
|
87
|
+
const entries = await input.backend.listFolder(folderId || undefined);
|
|
88
|
+
const existing = entries.find(
|
|
89
|
+
(e) => e.kind === "query" && e.label.trim().toLowerCase() === label.trim().toLowerCase(),
|
|
90
|
+
);
|
|
91
|
+
if (existing?.id) return existing.id;
|
|
92
|
+
} catch {
|
|
93
|
+
// If listing fails for any reason, fall back to creating a new managed query.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return mintSparqlManagedQueryIri(input.workspaceIri || "");
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await input.backend.writeQuery(resolvedQueryId, input.queryText, {
|
|
101
|
+
message: input.message,
|
|
102
|
+
expectedVersionTag: input.expectedVersionTag,
|
|
103
|
+
...(input.backendType === "sparql"
|
|
104
|
+
? {
|
|
105
|
+
label: label || undefined,
|
|
106
|
+
folderId: folderId || undefined,
|
|
107
|
+
associatedEndpoint: input.associatedEndpoint || undefined,
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
});
|
|
111
|
+
} catch (e) {
|
|
112
|
+
const err = asWorkspaceBackendError(e);
|
|
113
|
+
if (err.code === "CONFLICT") {
|
|
114
|
+
const extra =
|
|
115
|
+
input.backendType === "git"
|
|
116
|
+
? " Resolve the conflict externally (e.g., pull/rebase/merge) and then try saving again."
|
|
117
|
+
: " Refresh the query and try again.";
|
|
118
|
+
throw new Error(`Save conflict.${extra}`);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const read = await input.backend.readQuery(resolvedQueryId);
|
|
124
|
+
const lastSavedTextHash = hashQueryText(read.queryText);
|
|
125
|
+
|
|
126
|
+
const managedMetadata: ManagedTabMetadata = {
|
|
127
|
+
workspaceId: input.workspaceId,
|
|
128
|
+
backendType: input.backendType,
|
|
129
|
+
queryRef: input.backendType === "git" ? { path: resolvedQueryId } : { managedQueryIri: resolvedQueryId },
|
|
130
|
+
lastSavedVersionRef: versionRefFromVersionTag(input.backendType, read.versionTag),
|
|
131
|
+
lastSavedTextHash,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { queryId: resolvedQueryId, managedMetadata };
|
|
135
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeQueryText(queryText: string): string {
|
|
2
|
+
return queryText.replace(/\r\n/g, "\n").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function hashQueryText(queryText: string): string {
|
|
6
|
+
const text = normalizeQueryText(queryText);
|
|
7
|
+
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
hash ^= text.charCodeAt(i);
|
|
11
|
+
hash = (hash * 0x01000193) >>> 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return hash.toString(16).padStart(8, "0");
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type BackendType = "git" | "sparql";
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceConfigBase {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
type: BackendType;
|
|
8
|
+
createdAt?: string;
|
|
9
|
+
updatedAt?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GitWorkspaceAuthPat {
|
|
13
|
+
type: "pat";
|
|
14
|
+
token: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GitWorkspaceConfig extends WorkspaceConfigBase {
|
|
19
|
+
type: "git";
|
|
20
|
+
remoteUrl: string;
|
|
21
|
+
branch: string;
|
|
22
|
+
rootPath: string;
|
|
23
|
+
auth: GitWorkspaceAuthPat;
|
|
24
|
+
/** Optional hint for selecting a git provider client. Defaults to auto-detection. */
|
|
25
|
+
provider?: "auto" | "github" | "gitlab" | "bitbucket" | "gitea";
|
|
26
|
+
/** Optional override for provider API base URL (useful for self-hosted/enterprise instances). */
|
|
27
|
+
apiBaseUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SparqlWorkspaceConfig extends WorkspaceConfigBase {
|
|
31
|
+
type: "sparql";
|
|
32
|
+
endpoint: string;
|
|
33
|
+
workspaceIri: string;
|
|
34
|
+
defaultGraph?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type WorkspaceConfig = GitWorkspaceConfig | SparqlWorkspaceConfig;
|
|
38
|
+
|
|
39
|
+
export interface FolderEntry {
|
|
40
|
+
kind: "folder" | "query";
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
parentId?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ReadResult {
|
|
47
|
+
queryText: string;
|
|
48
|
+
versionTag?: string;
|
|
49
|
+
associatedEndpoint?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface VersionInfo {
|
|
54
|
+
id: string;
|
|
55
|
+
createdAt: string;
|
|
56
|
+
author?: string;
|
|
57
|
+
message?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WriteQueryOptions {
|
|
61
|
+
message?: string;
|
|
62
|
+
expectedVersionTag?: string;
|
|
63
|
+
/** Optional label for backends where the query ID is not derived from the label (e.g., SPARQL workspace). */
|
|
64
|
+
label?: string;
|
|
65
|
+
/** Optional folder id/path for backends where folder is not encoded in the query ID (e.g., SPARQL workspace). */
|
|
66
|
+
folderId?: string;
|
|
67
|
+
/** Optional associated endpoint for this saved version (e.g., SPARQL query execution endpoint). */
|
|
68
|
+
associatedEndpoint?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type GitQueryRef = { path: string };
|
|
72
|
+
export type SparqlQueryRef = { managedQueryIri: string };
|
|
73
|
+
export type QueryRef = GitQueryRef | SparqlQueryRef;
|
|
74
|
+
|
|
75
|
+
export type GitVersionRef = { commitSha?: string };
|
|
76
|
+
export type SparqlVersionRef = { managedQueryVersionIri: string };
|
|
77
|
+
export type VersionRef = GitVersionRef | SparqlVersionRef;
|
|
78
|
+
|
|
79
|
+
export interface ManagedTabMetadata {
|
|
80
|
+
workspaceId: string;
|
|
81
|
+
backendType: BackendType;
|
|
82
|
+
queryRef: QueryRef;
|
|
83
|
+
lastSavedVersionRef?: VersionRef;
|
|
84
|
+
lastSavedTextHash?: string;
|
|
85
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { WorkspaceConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface WorkspaceValidationResult {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isNonEmpty(value: string | undefined): boolean {
|
|
9
|
+
return !!value && value.trim().length > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validateWorkspaceConfig(config: WorkspaceConfig): WorkspaceValidationResult {
|
|
13
|
+
const errors: string[] = [];
|
|
14
|
+
|
|
15
|
+
if (!isNonEmpty(config.id)) errors.push("Workspace id is required");
|
|
16
|
+
if (!isNonEmpty(config.label)) errors.push("Workspace label is required");
|
|
17
|
+
|
|
18
|
+
if (config.type === "git") {
|
|
19
|
+
if (!isNonEmpty(config.remoteUrl)) errors.push("Git remote URL is required");
|
|
20
|
+
// rootPath can be empty string to mean repo root
|
|
21
|
+
if (typeof config.rootPath !== "string") errors.push("Git rootPath must be a string");
|
|
22
|
+
if (!config.auth || config.auth.type !== "pat") errors.push("Git auth is required");
|
|
23
|
+
|
|
24
|
+
const anyCfg = config as any;
|
|
25
|
+
if (anyCfg.provider && !["auto", "github", "gitlab", "bitbucket", "gitea"].includes(anyCfg.provider)) {
|
|
26
|
+
errors.push("Git provider must be one of: auto, github, gitlab, bitbucket, gitea");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (anyCfg.apiBaseUrl && typeof anyCfg.apiBaseUrl !== "string") {
|
|
30
|
+
errors.push("Git apiBaseUrl must be a string");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (config.type === "sparql") {
|
|
35
|
+
if (!isNonEmpty(config.endpoint)) errors.push("SPARQL endpoint is required");
|
|
36
|
+
if (!isNonEmpty(config.workspaceIri)) errors.push("Workspace IRI is required");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { valid: errors.length === 0, errors };
|
|
40
|
+
}
|
package/src/tab.scss
CHANGED
|
@@ -140,35 +140,25 @@
|
|
|
140
140
|
padding: 6px;
|
|
141
141
|
cursor: pointer;
|
|
142
142
|
color: var(--yasgui-button-text, #505050);
|
|
143
|
-
fill: var(--yasgui-button-text, #505050);
|
|
144
143
|
display: flex;
|
|
145
144
|
align-items: center;
|
|
146
145
|
justify-content: center;
|
|
147
146
|
flex-shrink: 0;
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
height: 15px;
|
|
152
|
-
font-family: initial;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
svg {
|
|
156
|
-
width: 20px;
|
|
157
|
-
height: 20px;
|
|
148
|
+
i {
|
|
149
|
+
font-size: 16px;
|
|
158
150
|
}
|
|
159
151
|
|
|
160
152
|
&:hover {
|
|
161
153
|
color: var(--yasgui-button-hover, black);
|
|
162
|
-
fill: var(--yasgui-button-hover, black);
|
|
163
154
|
}
|
|
164
155
|
|
|
165
156
|
// Responsive spacing - reduce padding on smaller screens
|
|
166
157
|
@media (max-width: 768px) {
|
|
167
158
|
padding: 2px;
|
|
168
159
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
height: 18px;
|
|
160
|
+
i {
|
|
161
|
+
font-size: 18px;
|
|
172
162
|
}
|
|
173
163
|
}
|
|
174
164
|
}
|