mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
feat: JSON visualiser (#178)
* feat: improved JSON visualiser to not analyse big files without confirmation * chore: fixes * chore: added changeset
This commit is contained in:
parent
403e9f3d0c
commit
1bf5e8eb8a
8 changed files with 735 additions and 25 deletions
5
.changeset/tame-turtles-obey.md
Normal file
5
.changeset/tame-turtles-obey.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"sqlseal": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
reworked JSONViewer to allow for visual JSONPath preview
|
||||||
|
|
@ -17,7 +17,7 @@ export class SettingsInit {
|
||||||
viewPluginGenerator: ViewPluginGeneratorType
|
viewPluginGenerator: ViewPluginGeneratorType
|
||||||
) {
|
) {
|
||||||
const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator);
|
const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator);
|
||||||
const jsonControl = new SettingsJsonControls(settings, app, plugin);
|
const jsonControl = new SettingsJsonControls(settings, app, plugin, viewPluginGenerator);
|
||||||
|
|
||||||
const controls = [csvControl, jsonControl];
|
const controls = [csvControl, jsonControl];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,18 @@ SELECT * FROM ${tableName}
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
\`\`\``
|
\`\`\``
|
||||||
|
|
||||||
|
const jsonQuery = (tableName: string, path: string, jsonPath: string) => {
|
||||||
|
const escapedJsonPath = jsonPath.replace(/"/g, '\\"');
|
||||||
|
return `\`\`\`sqlseal
|
||||||
|
TABLE ${tableName} = file(${path}, "${escapedJsonPath}")
|
||||||
|
|
||||||
|
SELECT * FROM ${tableName}
|
||||||
|
LIMIT 100
|
||||||
|
\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
export class CodeSampleModal extends Modal {
|
export class CodeSampleModal extends Modal {
|
||||||
constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType) {
|
constructor(app: App, private file: TFile, private viewPluginGenerator: ViewPluginGeneratorType, private jsonPath?: string) {
|
||||||
super(app);
|
super(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,7 +34,10 @@ export class CodeSampleModal extends Modal {
|
||||||
|
|
||||||
// Setup actual editor here
|
// Setup actual editor here
|
||||||
const tableName = sanitise(this.file.basename)
|
const tableName = sanitise(this.file.basename)
|
||||||
const q = query(tableName, this.file.path)
|
const isJsonFile = this.file.extension === 'json' || this.file.extension === 'json5';
|
||||||
|
const q = isJsonFile && this.jsonPath
|
||||||
|
? jsonQuery(tableName, this.file.path, this.jsonPath)
|
||||||
|
: query(tableName, this.file.path)
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: q,
|
doc: q,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { App, Setting } from "obsidian";
|
import { App, Plugin, Setting } from "obsidian";
|
||||||
import { Settings } from "../Settings";
|
import { Settings } from "../Settings";
|
||||||
import { SettingsControls } from "./SettingsControls";
|
import { SettingsControls } from "./SettingsControls";
|
||||||
import { checkTypeViewAvaiability } from "../utils/viewInspector";
|
import { checkTypeViewAvaiability } from "../utils/viewInspector";
|
||||||
|
|
@ -7,10 +7,15 @@ import {
|
||||||
JSON_VIEW_TYPE,
|
JSON_VIEW_TYPE,
|
||||||
JsonView,
|
JsonView,
|
||||||
} from "../view/JsonView";
|
} from "../view/JsonView";
|
||||||
|
import { ViewPluginGeneratorType } from "../../syntaxHighlight/viewPluginGenerator";
|
||||||
|
|
||||||
export class SettingsJsonControls extends SettingsControls {
|
export class SettingsJsonControls extends SettingsControls {
|
||||||
private registeredView: string | null = null;
|
private registeredView: string | null = null;
|
||||||
|
|
||||||
|
constructor(settings: Settings, app: App, plugin: Plugin, private viewPluginGenerator: ViewPluginGeneratorType) {
|
||||||
|
super(settings, app, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
register() {
|
register() {
|
||||||
if (this.settings.get("enableJSONViewer")) {
|
if (this.settings.get("enableJSONViewer")) {
|
||||||
const view = checkTypeViewAvaiability(this.app, JSON_VIEW_EXTENSIONS[0]);
|
const view = checkTypeViewAvaiability(this.app, JSON_VIEW_EXTENSIONS[0]);
|
||||||
|
|
@ -20,7 +25,7 @@ export class SettingsJsonControls extends SettingsControls {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf));
|
this.plugin.registerView(JSON_VIEW_TYPE, (leaf) => new JsonView(leaf, this.viewPluginGenerator));
|
||||||
this.plugin.registerExtensions(JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE);
|
this.plugin.registerExtensions(JSON_VIEW_EXTENSIONS, JSON_VIEW_TYPE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,253 @@
|
||||||
import { WorkspaceLeaf, TextFileView, Menu, IconName } from 'obsidian';
|
import { WorkspaceLeaf, TextFileView, Menu, IconName, Notice, ButtonComponent } from 'obsidian';
|
||||||
import { parse, stringify } from 'json5'
|
import { parse, stringify } from 'json5';
|
||||||
|
import * as jsonpath from 'jsonpath';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import { CodeSampleModal } from '../modal/showCodeSample';
|
||||||
|
import { ViewPluginGeneratorType } from '../../syntaxHighlight/viewPluginGenerator';
|
||||||
|
|
||||||
export const JSON_VIEW_TYPE = "sqlseal-json-viewer";
|
export const JSON_VIEW_TYPE = "sqlseal-json-viewer";
|
||||||
export const JSON_VIEW_EXTENSIONS = ['json', 'json5'];
|
export const JSON_VIEW_EXTENSIONS = ['json', 'json5'];
|
||||||
|
|
||||||
|
class JsonRenderer {
|
||||||
|
applySyntaxHighlighting(codeElement: HTMLElement): void {
|
||||||
|
const text = codeElement.textContent || '';
|
||||||
|
codeElement.innerHTML = text
|
||||||
|
.replace(/"([^"]+)":/g, '<span class="json-key">"$1":</span>')
|
||||||
|
.replace(/: (true|false)/g, ': <span class="json-boolean">$1</span>')
|
||||||
|
.replace(/: (null)/g, ': <span class="json-null">$1</span>')
|
||||||
|
.replace(/: (-?\d+(?:\.\d+)?)/g, ': <span class="json-number">$1</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFormattedJson(container: HTMLElement, parsedData: any): void {
|
||||||
|
container.empty();
|
||||||
|
const pre = container.createEl('pre');
|
||||||
|
const code = pre.createEl('code', { cls: 'language-json' });
|
||||||
|
|
||||||
|
// Enable text selection like a textarea
|
||||||
|
pre.style.userSelect = 'text';
|
||||||
|
pre.style.cursor = 'text';
|
||||||
|
code.style.userSelect = 'text';
|
||||||
|
|
||||||
|
const formatted = stringify(parsedData, null, 2);
|
||||||
|
if (formatted.length > 50000) {
|
||||||
|
const truncated = formatted.substring(0, 50000) + '\n\n... (content truncated for display)';
|
||||||
|
code.textContent = truncated;
|
||||||
|
} else {
|
||||||
|
code.textContent = formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applySyntaxHighlighting(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TableRenderer {
|
||||||
|
processTablePreview(
|
||||||
|
container: HTMLElement,
|
||||||
|
parsedData: any,
|
||||||
|
jsonPath: string,
|
||||||
|
isLargeFile: boolean
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
let queryResult = parsedData;
|
||||||
|
|
||||||
|
if (jsonPath !== '$') {
|
||||||
|
queryResult = jsonpath.query(parsedData, jsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(queryResult)) {
|
||||||
|
container.createEl('div', {
|
||||||
|
cls: 'sqlseal-info',
|
||||||
|
text: 'JSONPath result is not an array. Table preview requires array data.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryResult.length === 0) {
|
||||||
|
container.createEl('div', {
|
||||||
|
cls: 'sqlseal-info',
|
||||||
|
text: 'No data matches the JSONPath query.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderTable(container, queryResult, isLargeFile);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
const errorDiv = container.createEl('div', { cls: 'sqlseal-error' });
|
||||||
|
|
||||||
|
errorDiv.createEl('h4', { text: 'JSONPath Query Error' });
|
||||||
|
|
||||||
|
const errorMessage = e instanceof Error ? e.message : 'Invalid query syntax';
|
||||||
|
errorDiv.createEl('p', { text: `Error: ${errorMessage}` });
|
||||||
|
|
||||||
|
// Add some helpful tips
|
||||||
|
const tipsDiv = errorDiv.createEl('div', { cls: 'sqlseal-error-tips' });
|
||||||
|
tipsDiv.createEl('p', { text: 'Common JSONPath examples:' });
|
||||||
|
const tipsList = tipsDiv.createEl('ul');
|
||||||
|
tipsList.createEl('li', { text: '$ - Root object' });
|
||||||
|
tipsList.createEl('li', { text: '$.users[*] - All items in users array' });
|
||||||
|
tipsList.createEl('li', { text: '$.users[0] - First user' });
|
||||||
|
tipsList.createEl('li', { text: '$.users[?(@.active)] - Users where active is true' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderTable(container: HTMLElement, queryResult: any[], isLargeFile: boolean): void {
|
||||||
|
const maxRows = isLargeFile ? 50 : 100;
|
||||||
|
const displayRows = queryResult.slice(0, maxRows);
|
||||||
|
|
||||||
|
const sampleSize = Math.min(displayRows.length, 20);
|
||||||
|
const columns = uniq(displayRows.slice(0, sampleSize).map(row =>
|
||||||
|
typeof row === 'object' && row !== null ? Object.keys(row) : []
|
||||||
|
).flat());
|
||||||
|
|
||||||
|
if (columns.length === 0) {
|
||||||
|
container.createEl('div', {
|
||||||
|
cls: 'sqlseal-info',
|
||||||
|
text: 'Data contains no object properties to display in table.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = container.createEl('table', { cls: 'sqlseal-preview-table' });
|
||||||
|
|
||||||
|
this.createTableHeader(table, columns);
|
||||||
|
this.createTableBody(table, displayRows, columns);
|
||||||
|
this.addRowCountInfo(container, queryResult.length, maxRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTableHeader(table: HTMLElement, columns: string[]): void {
|
||||||
|
const thead = table.createEl('thead');
|
||||||
|
const headerRow = thead.createEl('tr');
|
||||||
|
columns.forEach(col => {
|
||||||
|
headerRow.createEl('th', { text: col });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTableBody(table: HTMLElement, displayRows: any[], columns: string[]): void {
|
||||||
|
const tbody = table.createEl('tbody');
|
||||||
|
displayRows.forEach((row) => {
|
||||||
|
const tableRow = tbody.createEl('tr');
|
||||||
|
columns.forEach(col => {
|
||||||
|
const cell = tableRow.createEl('td');
|
||||||
|
const value = typeof row === 'object' && row !== null ? row[col] : '';
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
cell.textContent = '';
|
||||||
|
cell.addClass('sqlseal-null-cell');
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
cell.textContent = JSON.stringify(value);
|
||||||
|
cell.addClass('sqlseal-object-cell');
|
||||||
|
} else {
|
||||||
|
cell.textContent = String(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRowCountInfo(container: HTMLElement, totalRows: number, maxRows: number): void {
|
||||||
|
if (totalRows > maxRows) {
|
||||||
|
container.createEl('div', {
|
||||||
|
cls: 'sqlseal-row-info',
|
||||||
|
text: `Showing first ${maxRows} of ${totalRows} rows (limited for performance)`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
container.createEl('div', {
|
||||||
|
cls: 'sqlseal-row-info',
|
||||||
|
text: `${totalRows} rows`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UIBuilder {
|
||||||
|
createWarningSection(container: HTMLElement, sizeInMB: number): HTMLButtonElement {
|
||||||
|
const warningSection = container.createDiv({ cls: 'sqlseal-file-warning' });
|
||||||
|
warningSection.createEl('div', {
|
||||||
|
cls: 'sqlseal-warning-text',
|
||||||
|
text: `⚠️ Large file detected (~${sizeInMB}MB). Analysis disabled to prevent freezing.`
|
||||||
|
});
|
||||||
|
|
||||||
|
return warningSection.createEl('button', {
|
||||||
|
cls: 'sqlseal-analyze-btn',
|
||||||
|
text: 'Analyze File (may take time)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createInputSection(container: HTMLElement, currentPath: string, isDisabled: boolean): HTMLInputElement {
|
||||||
|
const inputSection = container.createDiv({ cls: 'sqlseal-json-input-section' });
|
||||||
|
inputSection.createEl('label', { text: 'JSONPath Query:' });
|
||||||
|
return inputSection.createEl('input', {
|
||||||
|
type: 'text',
|
||||||
|
value: currentPath,
|
||||||
|
placeholder: '$.users[*] or $.data.items[?(@.active)]',
|
||||||
|
disabled: isDisabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createContentPanels(container: HTMLElement): {
|
||||||
|
jsonContainer: HTMLElement;
|
||||||
|
tableContainer: HTMLElement;
|
||||||
|
} {
|
||||||
|
const contentArea = container.createDiv({ cls: 'sqlseal-json-content' });
|
||||||
|
|
||||||
|
const jsonPanel = contentArea.createDiv({ cls: 'sqlseal-json-panel' });
|
||||||
|
const jsonContainer = jsonPanel.createDiv({ cls: 'sqlseal-json-display' });
|
||||||
|
|
||||||
|
const tablePanel = contentArea.createDiv({ cls: 'sqlseal-table-panel' });
|
||||||
|
const tableContainer = tablePanel.createDiv({ cls: 'sqlseal-table-display' });
|
||||||
|
|
||||||
|
return { jsonContainer, tableContainer };
|
||||||
|
}
|
||||||
|
|
||||||
|
createGenerateCodeButton(container: HTMLElement, onGenerate: () => void): HTMLElement {
|
||||||
|
const buttonContainer = container.createDiv({ cls: 'sqlseal-button-container' });
|
||||||
|
|
||||||
|
const buttonComponent = new ButtonComponent(buttonContainer);
|
||||||
|
buttonComponent
|
||||||
|
.setButtonText('Generate SQLSeal Code')
|
||||||
|
.onClick(onGenerate);
|
||||||
|
|
||||||
|
return buttonComponent.buttonEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
showPlaceholders(jsonContainer: HTMLElement, tableContainer: HTMLElement): void {
|
||||||
|
jsonContainer.createEl('div', {
|
||||||
|
cls: 'sqlseal-placeholder',
|
||||||
|
text: 'Click "Analyze File" above to parse and display the JSON content.'
|
||||||
|
});
|
||||||
|
|
||||||
|
tableContainer.createEl('div', {
|
||||||
|
cls: 'sqlseal-placeholder',
|
||||||
|
text: 'Table preview will be available after analysis.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LARGE_FILE_THRESHOLD = 1000000;
|
||||||
|
|
||||||
|
function analyzeFileSize(content: string): { isLarge: boolean; sizeInMB: number } {
|
||||||
|
const isLarge = content.length > LARGE_FILE_THRESHOLD;
|
||||||
|
const sizeInMB = parseFloat((content.length / 1000000).toFixed(1));
|
||||||
|
return { isLarge, sizeInMB };
|
||||||
|
}
|
||||||
|
|
||||||
export class JsonView extends TextFileView {
|
export class JsonView extends TextFileView {
|
||||||
private content: string = '';
|
private content: string = '';
|
||||||
private table: HTMLTableElement | undefined = undefined;
|
private parsedData: any = null;
|
||||||
|
private jsonPathInput: HTMLInputElement | undefined = undefined;
|
||||||
|
private jsonContainer: HTMLElement | undefined = undefined;
|
||||||
|
private tableContainer: HTMLElement | undefined = undefined;
|
||||||
|
private currentJsonPath: string = '$';
|
||||||
|
private isLargeFile: boolean = false;
|
||||||
|
private isAnalyzed: boolean = false;
|
||||||
|
private analyzeButton: HTMLButtonElement | undefined = undefined;
|
||||||
|
private isAnalyzing: boolean = false;
|
||||||
|
private generateCodeButton: HTMLButtonElement | undefined = undefined;
|
||||||
|
|
||||||
|
private jsonRenderer = new JsonRenderer();
|
||||||
|
private tableRenderer = new TableRenderer();
|
||||||
|
private uiBuilder = new UIBuilder();
|
||||||
|
|
||||||
constructor(leaf: WorkspaceLeaf) {
|
constructor(leaf: WorkspaceLeaf, private readonly viewPluginGenerator: ViewPluginGeneratorType) {
|
||||||
super(leaf);
|
super(leaf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +265,11 @@ export class JsonView extends TextFileView {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onOpen() {
|
async onOpen() {
|
||||||
|
// Ensure we have fresh file data when reopening
|
||||||
|
if (this.file) {
|
||||||
|
const fileContent = await this.app.vault.cachedRead(this.file);
|
||||||
|
this.content = fileContent;
|
||||||
|
}
|
||||||
this.renderJson();
|
this.renderJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +282,9 @@ export class JsonView extends TextFileView {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewData(data: string, clear: boolean): Promise<void> {
|
async setViewData(data: string, clear: boolean): Promise<void> {
|
||||||
|
if (clear) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
this.content = data;
|
this.content = data;
|
||||||
await this.renderJson();
|
await this.renderJson();
|
||||||
}
|
}
|
||||||
|
|
@ -48,29 +295,155 @@ export class JsonView extends TextFileView {
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.content = '';
|
this.content = '';
|
||||||
this.table?.empty();
|
this.parsedData = null;
|
||||||
|
this.isLargeFile = false;
|
||||||
|
this.isAnalyzed = false;
|
||||||
|
this.isAnalyzing = false;
|
||||||
|
this.currentJsonPath = '$';
|
||||||
|
this.jsonContainer?.empty();
|
||||||
|
this.tableContainer?.empty();
|
||||||
|
this.contentEl.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
api: any = null;
|
api: any = null;
|
||||||
|
|
||||||
private async renderJson() {
|
private async renderJson() {
|
||||||
|
|
||||||
if (!this.content) {
|
if (!this.content) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.contentEl.empty();
|
||||||
this.contentEl.empty()
|
|
||||||
const csvEditorDiv = this.contentEl.createDiv({ cls: 'sqlseal-json-editor' })
|
const { isLarge, sizeInMB } = analyzeFileSize(this.content);
|
||||||
const code = csvEditorDiv.createEl('pre').createEl('code')
|
this.isLargeFile = isLarge;
|
||||||
|
|
||||||
try {
|
const mainContainer = this.contentEl.createDiv({ cls: 'sqlseal-json-viewer' });
|
||||||
const data = parse(this.content)
|
|
||||||
const formatted = stringify(data, null, 2)
|
if (this.isLargeFile) {
|
||||||
code.appendText(formatted)
|
this.analyzeButton = this.uiBuilder.createWarningSection(mainContainer, sizeInMB);
|
||||||
} catch (e) {
|
this.analyzeButton.addEventListener('click', () => {
|
||||||
console.error(e)
|
this.performAnalysis();
|
||||||
// EMPTY
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jsonPathInput = this.uiBuilder.createInputSection(
|
||||||
|
mainContainer,
|
||||||
|
this.currentJsonPath,
|
||||||
|
this.isLargeFile && !this.isAnalyzed
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.isLargeFile || this.isAnalyzed) {
|
||||||
|
this.jsonPathInput.addEventListener('input', () => {
|
||||||
|
this.currentJsonPath = this.jsonPathInput!.value || '$';
|
||||||
|
this.updateTablePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generateCodeButton = this.uiBuilder.createGenerateCodeButton(mainContainer, () => this.generateSQLSealCode());
|
||||||
|
if (this.isLargeFile && !this.isAnalyzed) {
|
||||||
|
this.generateCodeButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { jsonContainer, tableContainer } = this.uiBuilder.createContentPanels(mainContainer);
|
||||||
|
this.jsonContainer = jsonContainer;
|
||||||
|
this.tableContainer = tableContainer;
|
||||||
|
|
||||||
|
if (!this.isLargeFile) {
|
||||||
|
await this.performAnalysis();
|
||||||
|
} else {
|
||||||
|
this.uiBuilder.showPlaceholders(this.jsonContainer, this.tableContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async performAnalysis() {
|
||||||
|
if (this.isAnalyzing || this.isAnalyzed) return;
|
||||||
|
|
||||||
|
this.isAnalyzing = true;
|
||||||
|
|
||||||
|
// Update button state
|
||||||
|
if (this.analyzeButton) {
|
||||||
|
this.analyzeButton.textContent = 'Analyzing...';
|
||||||
|
this.analyzeButton.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable input
|
||||||
|
if (this.jsonPathInput) {
|
||||||
|
this.jsonPathInput.disabled = false;
|
||||||
|
this.jsonPathInput.addEventListener('input', () => {
|
||||||
|
this.currentJsonPath = this.jsonPathInput!.value || '$';
|
||||||
|
this.updateTablePreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use setTimeout to allow UI to update
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
this.parsedData = parse(this.content);
|
||||||
|
this.isAnalyzed = true;
|
||||||
|
this.jsonRenderer.renderFormattedJson(this.jsonContainer!, this.parsedData);
|
||||||
|
this.updateTablePreview();
|
||||||
|
|
||||||
|
// Show generate code button for large files
|
||||||
|
if (this.generateCodeButton && this.isLargeFile) {
|
||||||
|
this.generateCodeButton.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide warning section
|
||||||
|
const warningSection = this.contentEl.querySelector('.sqlseal-file-warning');
|
||||||
|
if (warningSection) {
|
||||||
|
warningSection.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing JSON:', e);
|
||||||
|
this.jsonContainer!.empty();
|
||||||
|
this.jsonContainer!.createEl('div', {
|
||||||
|
cls: 'sqlseal-error',
|
||||||
|
text: `Error parsing JSON: ${e instanceof Error ? e.message : 'Unknown error'}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isAnalyzing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTablePreview() {
|
||||||
|
if (!this.tableContainer || !this.parsedData || this.isAnalyzing) return;
|
||||||
|
|
||||||
|
this.tableContainer.empty();
|
||||||
|
|
||||||
|
if (this.isLargeFile) {
|
||||||
|
const loadingDiv = this.tableContainer.createEl('div', {
|
||||||
|
cls: 'sqlseal-loading',
|
||||||
|
text: 'Processing JSONPath query...'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loadingDiv.remove();
|
||||||
|
this.tableRenderer.processTablePreview(
|
||||||
|
this.tableContainer!,
|
||||||
|
this.parsedData,
|
||||||
|
this.currentJsonPath,
|
||||||
|
this.isLargeFile
|
||||||
|
);
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
this.tableRenderer.processTablePreview(
|
||||||
|
this.tableContainer,
|
||||||
|
this.parsedData,
|
||||||
|
this.currentJsonPath,
|
||||||
|
this.isLargeFile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSQLSealCode(): void {
|
||||||
|
if (!this.file) {
|
||||||
|
new Notice('No file available for code generation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new CodeSampleModal(this.app, this.file, this.viewPluginGenerator, this.currentJsonPath);
|
||||||
|
modal.open();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import { ISyncStrategy } from "./abstractSyncStrategy";
|
||||||
import { ParserTableDefinition } from "./types";
|
import { ParserTableDefinition } from "./types";
|
||||||
import { parse } from 'json5';
|
import { parse } from 'json5';
|
||||||
import { uniq } from "lodash";
|
import { uniq } from "lodash";
|
||||||
import * as jsonpath from 'jsonpath'
|
import * as jsonpath from 'jsonpath';
|
||||||
import { FilepathHasher } from "../../../utils/hasher";
|
import { FilepathHasher } from "../../../utils/hasher";
|
||||||
|
|
||||||
const DEFAULT_FILE_HASH = ''
|
const DEFAULT_FILE_HASH = ''
|
||||||
|
|
|
||||||
313
src/styles/jsonViewer.scss
Normal file
313
src/styles/jsonViewer.scss
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
// JSON Viewer Styles
|
||||||
|
.sqlseal-json-viewer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.sqlseal-file-warning {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--background-modifier-cover);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.sqlseal-warning-text {
|
||||||
|
color: var(--text-warning);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-analyze-btn {
|
||||||
|
background: var(--interactive-accent);
|
||||||
|
color: var(--text-on-accent);
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--interactive-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--interactive-normal);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-json-input-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--interactive-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-button-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-json-content {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-json-panel,
|
||||||
|
.sqlseal-table-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--background-primary);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border);
|
||||||
|
background: var(--background-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-json-display {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
// JSON syntax highlighting
|
||||||
|
.json-key {
|
||||||
|
color: var(--color-blue);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-string {
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-number {
|
||||||
|
color: var(--color-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-boolean {
|
||||||
|
color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-null {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-table-display {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.sqlseal-preview-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 2px solid var(--background-modifier-border);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-normal);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-bottom: 1px solid var(--background-modifier-border-hover);
|
||||||
|
color: var(--text-normal);
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
&.sqlseal-null-cell {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sqlseal-object-cell {
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
color: var(--text-accent);
|
||||||
|
font-size: 11px;
|
||||||
|
max-width: 200px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-row-info {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-error,
|
||||||
|
.sqlseal-info,
|
||||||
|
.sqlseal-placeholder,
|
||||||
|
.sqlseal-loading {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-error {
|
||||||
|
background: var(--background-modifier-error);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid var(--background-modifier-error-border);
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 4px 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-error-tips {
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 6px 0 0 16px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 2px 0;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-info {
|
||||||
|
background: var(--background-modifier-cover);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-placeholder {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqlseal-loading {
|
||||||
|
background: var(--background-modifier-cover);
|
||||||
|
color: var(--text-accent);
|
||||||
|
border: 1px solid var(--interactive-accent);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
border: 2px solid var(--text-accent);
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading spinner animation
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive design for smaller screens
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.sqlseal-json-viewer .sqlseal-json-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 300px 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,5 +9,6 @@
|
||||||
@use 'canvas';
|
@use 'canvas';
|
||||||
@use 'autocomplete';
|
@use 'autocomplete';
|
||||||
@use 'global-tables';
|
@use 'global-tables';
|
||||||
|
@use 'jsonViewer';
|
||||||
|
|
||||||
@use '../modules/explorer/explorer/style.scss';
|
@use '../modules/explorer/explorer/style.scss';
|
||||||
Loading…
Reference in a new issue