Merge pull request #10 from h-sphere/feat/global-configurations

feat: adding global configurations
This commit is contained in:
Kacper Kula 2025-08-14 13:03:53 +02:00 committed by GitHub
commit 7a7db26abb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 665 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal-charts": minor
---
adding support for global configurations

View file

@ -35,6 +35,7 @@
"@types/acorn": "^6.0.4",
"@types/estree": "^1.0.6",
"@types/jest": "^29.5.14",
"@types/json5": "^2.2.0",
"@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",

View file

@ -60,6 +60,9 @@ importers:
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
'@types/json5':
specifier: ^2.2.0
version: 2.2.0
'@types/node':
specifier: ^22.13.10
version: 22.13.10
@ -1062,6 +1065,10 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/json5@2.2.0':
resolution: {integrity: sha512-NrVug5woqbvNZ0WX+Gv4R+L4TGddtmFek2u8RtccAgFZWtS9QXF2xCXY22/M4nzkaKF0q9Fc6M/5rxLDhfwc/A==}
deprecated: This is a stub types definition. json5 provides its own type definitions, so you do not need this installed.
'@types/leaflet@1.9.16':
resolution: {integrity: sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==}
@ -3873,6 +3880,10 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/json5@2.2.0':
dependencies:
json5: 2.2.3
'@types/leaflet@1.9.16':
dependencies:
'@types/geojson': 7946.0.16

View file

@ -7,6 +7,8 @@ import type { RendererConfig } from "@hypersphere/sqlseal";
import { ViewDefinition } from "@hypersphere/sqlseal/dist/src/grammar/parser";
import { parseCodeAdvanced } from "./utils/advancedParser";
import { FullScreenChartModal } from "./fullscreenModal";
import { SQLSealChartsSettings } from "./settings";
import * as JSON5 from "json5";
interface Config {
config: string
@ -18,7 +20,7 @@ echarts.registerTransform((ecStat as any).transform.histogram);
export class ChartRenderer implements RendererConfig {
constructor(private readonly app: App) {
constructor(private readonly app: App, private readonly settings: SQLSealChartsSettings) {
}
get viewDefinition(): ViewDefinition {
@ -51,6 +53,23 @@ export class ChartRenderer implements RendererConfig {
})
}
private prepareGlobalConfigVariables(): Record<string, any> {
const globalVariables: Record<string, any> = {};
this.settings.globalConfigs.forEach(config => {
try {
const parsedConfig = JSON5.parse(config.config);
globalVariables[config.name] = parsedConfig;
} catch (e) {
console.warn(`Failed to parse global configuration '${config.name}' with JSON5:`, e);
// Still make it available as a string if JSON5 parsing fails
globalVariables[config.name] = config.config;
}
});
return globalVariables;
}
render(config: Config, el: HTMLElement) {
let isRendered: boolean = false
let chart: echarts.ECharts | null = null
@ -64,17 +83,21 @@ export class ChartRenderer implements RendererConfig {
throw new Error('To process JavaScript, set ADVANCED MODE flag')
}
const { functions, variables } = prepareDataVariables({ columns, data })
// Add global configurations as variables
const globalVariables = this.prepareGlobalConfigVariables()
const allVariables = { ...variables, ...globalVariables }
let parsedConfig: Object = {}
if (isAdvancedMode) {
try {
parsedConfig = parseCodeAdvanced({ functions, variables }, config.config)
parsedConfig = parseCodeAdvanced({ functions, variables: allVariables }, config.config)
} catch (e) {
console.error(e)
throw e
}
} else {
parsedConfig = parseCode(config.config, functions, variables) as Object
parsedConfig = parseCode(config.config, functions, allVariables) as Object
}
if (!parsedConfig || typeof parsedConfig !== 'object') {
throw new Error('Issue with parsing config')

View file

@ -3,6 +3,8 @@ import { ChartRenderer } from "./chartRenderer";
import { pluginApi } from "@vanakat/plugin-api";
import type { SQLSealApi, SQLSealRegisterApi } from "@hypersphere/sqlseal";
import { uniqBy } from "lodash";
import { SQLSealChartsSettings, DEFAULT_SETTINGS } from "./settings";
import { SQLSealChartsSettingTab } from "./settingsTab";
const SQLSEAL_API_KEY = "___sqlSeal";
const SQLSEAL_QUEUED_PLUGINS = "___sqlSeal_queue";
@ -38,14 +40,30 @@ const registerApi = (plugin: Plugin, fn: (api: SQLSealApi) => void) => {
};
export default class SQLSealCharts extends Plugin {
settings: SQLSealChartsSettings;
async onload() {
await this.loadSettings();
// Add settings tab
this.addSettingTab(new SQLSealChartsSettingTab(this.app, this));
registerApi(this, (api) => this.sqlSealRegistered(api));
}
sqlSealRegistered(api: SQLSealApi) {
api.registerView("sqlseal-charts", new ChartRenderer(this.app));
api.registerView("sqlseal-charts", new ChartRenderer(this.app, this.settings));
if ((api as any).apiVersion >= 2) {
api.registerFlag({ key: "isAdvancedMode", name: "ADVANCED MODE" });
}
}
async loadSettings() {
const data = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
}
async saveSettings() {
await this.saveData(this.settings);
}
}

37
src/settings.ts Normal file
View file

@ -0,0 +1,37 @@
export interface GlobalChartConfig {
name: string;
config: string; // JSON string
}
export interface SQLSealChartsSettings {
globalConfigs: GlobalChartConfig[];
}
export const DEFAULT_SETTINGS: SQLSealChartsSettings = {
globalConfigs: [
{
name: "defaultTheme",
config: `{
backgroundColor: "#ffffff",
textStyle: {
color: "#333333",
fontFamily: "Arial, sans-serif"
}
}`
},
{
name: "pieChart",
config: `{
type: 'pie',
radius: '70%',
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}`
}
]
};

343
src/settingsTab.ts Normal file
View file

@ -0,0 +1,343 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import SQLSealCharts from "./main";
import { GlobalChartConfig } from "./settings";
import * as JSON5 from "json5";
export class SQLSealChartsSettingTab extends PluginSettingTab {
plugin: SQLSealCharts;
private currentView: 'list' | 'edit' = 'list';
private editingIndex: number = -1;
constructor(app: App, plugin: SQLSealCharts) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'SQLSeal Charts Settings' });
if (this.currentView === 'list') {
this.displayConfigList();
} else if (this.currentView === 'edit') {
this.displayConfigEditor();
}
}
private displayConfigList(): void {
const { containerEl } = this;
containerEl.createEl('h3', { text: 'Global Chart Configurations' });
const descriptionContainer = containerEl.createDiv('sqlseal-description-container');
descriptionContainer.createEl('p', {
text: 'Define reusable JSON5 configurations that can be accessed as variables in your charts.',
cls: 'setting-item-description'
});
const exampleContainer = descriptionContainer.createDiv('sqlseal-example-container');
exampleContainer.createEl('strong', { text: 'Example Usage:' });
exampleContainer.createEl('br');
exampleContainer.createEl('code', {
text: '1. Create config named "theme" with: {backgroundColor: "#1a1a1a"}',
cls: 'sqlseal-example-code'
});
exampleContainer.createEl('br');
exampleContainer.createEl('code', {
text: '2. Use in charts: {...theme, title: {text: "My Chart"}}',
cls: 'sqlseal-example-code'
});
const featuresContainer = descriptionContainer.createDiv('sqlseal-features-container');
featuresContainer.createEl('br');
featuresContainer.createEl('small', {
text: '✨ Supports JSON5: unquoted keys, single quotes, trailing commas, and comments',
cls: 'sqlseal-feature-text'
});
// Add new configuration button
new Setting(containerEl)
.setName('Add New Configuration')
.setDesc('Create a new global configuration')
.addButton(button => {
button
.setButtonText('Add Configuration')
.setCta()
.onClick(() => {
this.editingIndex = -1; // -1 means new config
this.currentView = 'edit';
this.display();
});
});
// Create table
const tableContainer = containerEl.createDiv('sqlseal-config-table-container');
const table = tableContainer.createEl('table', { cls: 'sqlseal-config-table' });
// Table header
const thead = table.createEl('thead');
const headerRow = thead.createEl('tr');
headerRow.createEl('th', { text: 'Name' });
headerRow.createEl('th', { text: 'Preview' });
headerRow.createEl('th', { text: 'Actions' });
// Table body
const tbody = table.createEl('tbody');
if (this.plugin.settings.globalConfigs.length === 0) {
const emptyRow = tbody.createEl('tr');
const emptyCell = emptyRow.createEl('td', {
attr: { colspan: '3' },
cls: 'sqlseal-empty-state',
text: 'No configurations found. Click "Add Configuration" to create one.'
});
} else {
this.plugin.settings.globalConfigs.forEach((config, index) => {
this.createConfigRow(tbody, config, index);
});
}
}
private createConfigRow(tbody: HTMLTableSectionElement, config: GlobalChartConfig, index: number): void {
const row = tbody.createEl('tr');
// Name column
const nameCell = row.createEl('td');
nameCell.createEl('span', {
text: config.name,
cls: 'sqlseal-config-name'
});
// Preview column
const previewCell = row.createEl('td');
const previewText = this.getConfigPreview(config.config);
previewCell.createEl('code', {
text: previewText,
cls: 'sqlseal-config-preview'
});
// Actions column
const actionsCell = row.createEl('td', { cls: 'sqlseal-config-actions' });
const editButton = actionsCell.createEl('button', {
text: 'Edit',
cls: 'mod-cta sqlseal-action-button'
});
editButton.addEventListener('click', () => {
this.editingIndex = index;
this.currentView = 'edit';
this.display();
});
const deleteButton = actionsCell.createEl('button', {
text: 'Delete',
cls: 'mod-warning sqlseal-action-button'
});
deleteButton.addEventListener('click', async () => {
if (confirm(`Are you sure you want to delete the configuration "${config.name}"?`)) {
this.plugin.settings.globalConfigs.splice(index, 1);
await this.plugin.saveSettings();
this.display();
}
});
}
private displayConfigEditor(): void {
const { containerEl } = this;
const isNewConfig = this.editingIndex === -1;
const originalConfig = isNewConfig
? { name: `config${this.plugin.settings.globalConfigs.length + 1}`, config: '{\n \n}' }
: this.plugin.settings.globalConfigs[this.editingIndex];
// Create a working copy to avoid modifying the original until save
const config = {
name: originalConfig.name,
config: originalConfig.config
};
console.log('Editor debug - isNewConfig:', isNewConfig);
console.log('Editor debug - editingIndex:', this.editingIndex);
console.log('Editor debug - originalConfig:', originalConfig);
console.log('Editor debug - working config:', config);
console.log('Editor debug - all configs:', this.plugin.settings.globalConfigs);
// Header with back button
const headerContainer = containerEl.createDiv('sqlseal-editor-header');
const backButton = headerContainer.createEl('button', {
text: '← Back to List',
cls: 'sqlseal-back-button'
});
backButton.addEventListener('click', () => {
this.currentView = 'list';
this.display();
});
headerContainer.createEl('h3', {
text: isNewConfig ? 'Add New Configuration' : `Edit Configuration: ${config.name}`
});
// Name input
new Setting(containerEl)
.setName('Configuration Name')
.setDesc('A unique name to identify this configuration')
.addText(text => {
text.setValue(config.name);
text.onChange(async (value) => {
config.name = value;
// Don't auto-save for existing configs since we're using a working copy
});
// Save on blur
text.inputEl.addEventListener('blur', async () => {
const newValue = text.getValue();
config.name = newValue;
// Don't auto-save for existing configs since we're using a working copy
});
});
// JSON editor
const jsonContainer = containerEl.createDiv('sqlseal-json-editor-container');
const labelContainer = jsonContainer.createDiv('sqlseal-json-label-container');
labelContainer.createEl('label', {
text: 'JSON5 Configuration:',
cls: 'sqlseal-json-label'
});
labelContainer.createEl('small', {
text: 'Supports unquoted keys, single quotes, trailing commas, and // comments',
cls: 'sqlseal-json-help'
});
const jsonTextarea = jsonContainer.createEl('textarea', {
placeholder: `{
type: 'pie',
radius: '70%',
emphasis: {
itemStyle: {
shadowBlur: 10
}
}
}`,
cls: 'sqlseal-json-textarea'
});
// Set the value separately to ensure it loads properly
console.log('Setting textarea value to:', config.config);
jsonTextarea.value = config.config || '';
// Also set it after a short delay to ensure DOM is ready
requestAnimationFrame(() => {
jsonTextarea.value = config.config || '';
console.log('Textarea value after setting:', jsonTextarea.value);
});
// Auto-save on blur and input with debounce
let saveTimeout: NodeJS.Timeout;
const saveConfig = async () => {
try {
// Use JSON5 for more flexible parsing
JSON5.parse(jsonTextarea.value);
jsonTextarea.classList.remove('error');
jsonTextarea.title = '';
config.config = jsonTextarea.value;
// Don't auto-save since we're using a working copy
} catch (e) {
jsonTextarea.classList.add('error');
jsonTextarea.title = `Invalid JSON5: ${(e as Error).message}`;
}
};
jsonTextarea.addEventListener('input', () => {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(saveConfig, 500); // Debounce for 500ms
});
jsonTextarea.addEventListener('blur', saveConfig);
// Helper buttons
const helperContainer = containerEl.createDiv('sqlseal-helper-buttons');
const formatButton = helperContainer.createEl('button', {
text: 'Format JSON',
cls: 'mod-cta'
});
formatButton.addEventListener('click', () => {
try {
// Parse with JSON5 (allows unquoted keys, trailing commas, etc.)
const parsed = JSON5.parse(jsonTextarea.value);
// Format as standard JSON for consistency
const formatted = JSON.stringify(parsed, null, 2);
jsonTextarea.value = formatted;
config.config = formatted;
jsonTextarea.classList.remove('error');
jsonTextarea.title = '';
// Show success feedback
formatButton.textContent = '✓ Formatted';
formatButton.classList.add('sqlseal-format-success');
setTimeout(() => {
formatButton.textContent = 'Format JSON';
formatButton.classList.remove('sqlseal-format-success');
}, 1500);
} catch (e) {
formatButton.textContent = '✗ Invalid JSON5';
formatButton.classList.add('sqlseal-format-error');
setTimeout(() => {
formatButton.textContent = 'Format JSON';
formatButton.classList.remove('sqlseal-format-error');
}, 2000);
}
});
// Action buttons
const buttonContainer = containerEl.createDiv('sqlseal-editor-buttons');
const saveButton = buttonContainer.createEl('button', {
text: isNewConfig ? 'Create Configuration' : 'Save Changes',
cls: 'mod-cta'
});
saveButton.addEventListener('click', async () => {
try {
JSON5.parse(config.config);
if (isNewConfig) {
this.plugin.settings.globalConfigs.push(config);
} else {
// Update the original config with our working copy
this.plugin.settings.globalConfigs[this.editingIndex].name = config.name;
this.plugin.settings.globalConfigs[this.editingIndex].config = config.config;
}
await this.plugin.saveSettings();
this.currentView = 'list';
this.display();
} catch (e) {
alert(`Invalid JSON5: ${(e as Error).message}`);
}
});
const cancelButton = buttonContainer.createEl('button', {
text: 'Cancel',
cls: 'mod-warning'
});
cancelButton.addEventListener('click', () => {
this.currentView = 'list';
this.display();
});
}
private getConfigPreview(configString: string): string {
try {
const parsed = JSON5.parse(configString);
const preview = JSON.stringify(parsed);
return preview.length > 50 ? preview.substring(0, 47) + '...' : preview;
} catch (e) {
return 'Invalid JSON5';
}
}
}

View file

@ -78,4 +78,227 @@
width: 100%;
height: calc(90vh - 40px);
min-height: 400px;
}
/* Settings Panel Styles */
.sqlseal-config-table-container {
margin: 20px 0;
}
.sqlseal-config-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
overflow: hidden;
}
.sqlseal-config-table th,
.sqlseal-config-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--background-modifier-border);
}
.sqlseal-config-table th {
background: var(--background-modifier-hover);
font-weight: 600;
color: var(--text-normal);
}
.sqlseal-config-table tr:last-child td {
border-bottom: none;
}
.sqlseal-config-table tr:hover {
background: var(--background-modifier-hover-alt);
}
.sqlseal-config-name {
font-weight: 500;
color: var(--text-normal);
}
.sqlseal-config-preview {
font-family: var(--font-monospace);
font-size: 12px;
background: var(--background-modifier-border);
padding: 4px 8px;
border-radius: var(--radius-s);
color: var(--text-muted);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sqlseal-config-actions {
white-space: nowrap;
}
.sqlseal-action-button {
padding: 4px 12px;
margin-right: 8px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s ease;
}
.sqlseal-action-button:last-child {
margin-right: 0;
}
.sqlseal-empty-state {
text-align: center;
color: var(--text-muted);
font-style: italic;
padding: 40px 20px;
}
/* Editor Styles */
.sqlseal-editor-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 16px;
}
.sqlseal-back-button {
padding: 6px 12px;
background: var(--interactive-normal);
border: none;
border-radius: var(--radius-s);
cursor: pointer;
color: var(--text-normal);
font-size: 14px;
transition: background-color 0.2s ease;
}
.sqlseal-back-button:hover {
background: var(--interactive-hover);
}
.sqlseal-json-editor-container {
margin: 20px 0;
}
.sqlseal-json-label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-normal);
}
.sqlseal-json-textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
font-family: var(--font-monospace);
font-size: 13px;
line-height: 1.4;
resize: vertical;
transition: border-color 0.2s ease;
}
.sqlseal-json-textarea:focus {
border-color: var(--interactive-accent);
outline: none;
}
.sqlseal-json-textarea.error {
border-color: var(--text-error);
}
.sqlseal-editor-buttons {
display: flex;
gap: 12px;
justify-content: flex-start;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--background-modifier-border);
}
.sqlseal-editor-buttons button {
padding: 8px 16px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
}
/* Documentation Styles */
.sqlseal-description-container {
margin-bottom: 20px;
padding: 16px;
background: var(--background-secondary);
border-radius: var(--radius-s);
border-left: 4px solid var(--interactive-accent);
}
.sqlseal-example-container {
margin-top: 12px;
}
.sqlseal-example-code {
font-family: var(--font-monospace);
font-size: 12px;
background: var(--background-modifier-border);
padding: 2px 6px;
border-radius: var(--radius-xs);
color: var(--text-muted);
}
.sqlseal-feature-text {
color: var(--text-accent);
font-style: italic;
}
.sqlseal-json-label-container {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 8px;
}
.sqlseal-json-help {
color: var(--text-muted);
font-size: 11px;
font-style: italic;
}
/* Helper Button Styles */
.sqlseal-helper-buttons {
display: flex;
gap: 8px;
margin: 12px 0;
justify-content: flex-start;
}
.sqlseal-helper-buttons button {
padding: 6px 12px;
border: none;
border-radius: var(--radius-s);
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
/* Success and Error States for Format Button */
.sqlseal-format-success {
background-color: #4caf50 !important;
}
.sqlseal-format-error {
background-color: #ff6b6b !important;
}