- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
569 lines
20 KiB
JavaScript
569 lines
20 KiB
JavaScript
/**
|
|
* ComponentPlayground
|
|
*
|
|
* Interactive development tool for LiveComponents.
|
|
*
|
|
* Features:
|
|
* - Component browser with search and filtering
|
|
* - Live component preview with auto-refresh
|
|
* - JSON state editor with validation
|
|
* - Action tester with parameter support
|
|
* - Performance metrics visualization
|
|
* - Template code generator
|
|
*
|
|
* Usage:
|
|
* import { ComponentPlayground } from './modules/livecomponent/ComponentPlayground';
|
|
*
|
|
* const playground = new ComponentPlayground('#playground-container');
|
|
* playground.init();
|
|
*/
|
|
|
|
export class ComponentPlayground {
|
|
/**
|
|
* @param {string} containerSelector - Container element selector
|
|
*/
|
|
constructor(containerSelector) {
|
|
this.container = document.querySelector(containerSelector);
|
|
if (!this.container) {
|
|
throw new Error(`Container not found: ${containerSelector}`);
|
|
}
|
|
|
|
// State
|
|
this.components = [];
|
|
this.selectedComponent = null;
|
|
this.componentMetadata = null;
|
|
this.currentState = {};
|
|
this.previewInstanceId = `playground-${Date.now()}`;
|
|
|
|
// UI Elements (will be created in init())
|
|
this.componentList = null;
|
|
this.componentSearch = null;
|
|
this.stateEditor = null;
|
|
this.previewContainer = null;
|
|
this.actionTester = null;
|
|
this.metricsDisplay = null;
|
|
|
|
// Performance tracking
|
|
this.metrics = {
|
|
renderTime: 0,
|
|
stateSize: 0,
|
|
actionExecutions: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initialize playground
|
|
*/
|
|
async init() {
|
|
this.buildUI();
|
|
await this.loadComponents();
|
|
this.attachEventListeners();
|
|
}
|
|
|
|
/**
|
|
* Build playground UI structure
|
|
*/
|
|
buildUI() {
|
|
this.container.innerHTML = `
|
|
<div class="playground">
|
|
<!-- Header -->
|
|
<header class="playground__header">
|
|
<h1 class="playground__title">LiveComponent Playground</h1>
|
|
<p class="playground__subtitle">Interactive development tool for testing LiveComponents</p>
|
|
</header>
|
|
|
|
<!-- Main Layout -->
|
|
<div class="playground__layout">
|
|
<!-- Sidebar: Component Selector -->
|
|
<aside class="playground__sidebar">
|
|
<div class="playground__search">
|
|
<input
|
|
type="text"
|
|
id="component-search"
|
|
class="playground__search-input"
|
|
placeholder="Search components..."
|
|
autocomplete="off"
|
|
/>
|
|
</div>
|
|
<div id="component-list" class="playground__component-list">
|
|
<div class="playground__loading">Loading components...</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main class="playground__main">
|
|
<!-- State Editor -->
|
|
<section class="playground__section">
|
|
<h2 class="playground__section-title">Component State</h2>
|
|
<div class="playground__state-editor">
|
|
<textarea
|
|
id="state-editor"
|
|
class="playground__textarea"
|
|
placeholder='{\n "property": "value"\n}'
|
|
rows="10"
|
|
></textarea>
|
|
<div class="playground__editor-actions">
|
|
<button id="apply-state" class="playground__button playground__button--primary">
|
|
Apply State
|
|
</button>
|
|
<button id="reset-state" class="playground__button">
|
|
Reset
|
|
</button>
|
|
<button id="format-json" class="playground__button">
|
|
Format JSON
|
|
</button>
|
|
</div>
|
|
<div id="state-validation" class="playground__validation"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Live Preview -->
|
|
<section class="playground__section">
|
|
<h2 class="playground__section-title">Live Preview</h2>
|
|
<div id="preview-container" class="playground__preview">
|
|
<div class="playground__empty">
|
|
Select a component to preview
|
|
</div>
|
|
</div>
|
|
<div id="metrics-display" class="playground__metrics"></div>
|
|
</section>
|
|
|
|
<!-- Action Tester -->
|
|
<section class="playground__section">
|
|
<h2 class="playground__section-title">Actions</h2>
|
|
<div id="action-tester" class="playground__actions">
|
|
<div class="playground__empty">
|
|
Select a component to test actions
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Code Generator -->
|
|
<section class="playground__section">
|
|
<h2 class="playground__section-title">Template Code</h2>
|
|
<div class="playground__code-generator">
|
|
<pre id="generated-code" class="playground__code"><code>Select a component to generate code</code></pre>
|
|
<button id="copy-code" class="playground__button">
|
|
Copy to Clipboard
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Cache UI elements
|
|
this.componentList = this.container.querySelector('#component-list');
|
|
this.componentSearch = this.container.querySelector('#component-search');
|
|
this.stateEditor = this.container.querySelector('#state-editor');
|
|
this.previewContainer = this.container.querySelector('#preview-container');
|
|
this.actionTester = this.container.querySelector('#action-tester');
|
|
this.metricsDisplay = this.container.querySelector('#metrics-display');
|
|
}
|
|
|
|
/**
|
|
* Load all available components
|
|
*/
|
|
async loadComponents() {
|
|
try {
|
|
const response = await fetch('/playground/api/components');
|
|
const data = await response.json();
|
|
|
|
this.components = data.components || [];
|
|
this.renderComponentList(this.components);
|
|
} catch (error) {
|
|
this.componentList.innerHTML = `
|
|
<div class="playground__error">
|
|
Failed to load components: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render component list
|
|
*/
|
|
renderComponentList(components) {
|
|
if (components.length === 0) {
|
|
this.componentList.innerHTML = '<div class="playground__empty">No components found</div>';
|
|
return;
|
|
}
|
|
|
|
this.componentList.innerHTML = components.map(component => `
|
|
<div class="playground__component-item" data-component="${component.name}">
|
|
<div class="playground__component-name">${component.name}</div>
|
|
<div class="playground__component-meta">
|
|
<span class="playground__badge">${component.properties} props</span>
|
|
<span class="playground__badge">${component.actions} actions</span>
|
|
${component.has_cache ? '<span class="playground__badge playground__badge--cache">cached</span>' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
/**
|
|
* Attach event listeners
|
|
*/
|
|
attachEventListeners() {
|
|
// Component selection
|
|
this.componentList.addEventListener('click', (e) => {
|
|
const item = e.target.closest('.playground__component-item');
|
|
if (item) {
|
|
this.selectComponent(item.dataset.component);
|
|
}
|
|
});
|
|
|
|
// Component search
|
|
this.componentSearch.addEventListener('input', (e) => {
|
|
this.filterComponents(e.target.value);
|
|
});
|
|
|
|
// State editor actions
|
|
this.container.querySelector('#apply-state').addEventListener('click', () => {
|
|
this.applyState();
|
|
});
|
|
|
|
this.container.querySelector('#reset-state').addEventListener('click', () => {
|
|
this.resetState();
|
|
});
|
|
|
|
this.container.querySelector('#format-json').addEventListener('click', () => {
|
|
this.formatJSON();
|
|
});
|
|
|
|
// Copy code
|
|
this.container.querySelector('#copy-code').addEventListener('click', () => {
|
|
this.copyCode();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Filter components by search term
|
|
*/
|
|
filterComponents(searchTerm) {
|
|
const term = searchTerm.toLowerCase().trim();
|
|
|
|
if (!term) {
|
|
this.renderComponentList(this.components);
|
|
return;
|
|
}
|
|
|
|
const filtered = this.components.filter(component =>
|
|
component.name.toLowerCase().includes(term) ||
|
|
component.class.toLowerCase().includes(term)
|
|
);
|
|
|
|
this.renderComponentList(filtered);
|
|
}
|
|
|
|
/**
|
|
* Select component
|
|
*/
|
|
async selectComponent(componentName) {
|
|
// Update active state
|
|
this.componentList.querySelectorAll('.playground__component-item').forEach(item => {
|
|
item.classList.remove('playground__component-item--active');
|
|
});
|
|
|
|
const selectedItem = this.componentList.querySelector(`[data-component="${componentName}"]`);
|
|
if (selectedItem) {
|
|
selectedItem.classList.add('playground__component-item--active');
|
|
}
|
|
|
|
this.selectedComponent = componentName;
|
|
|
|
// Load metadata
|
|
await this.loadComponentMetadata(componentName);
|
|
|
|
// Reset state
|
|
this.resetState();
|
|
|
|
// Render actions
|
|
this.renderActions();
|
|
|
|
// Generate code
|
|
this.updateGeneratedCode();
|
|
}
|
|
|
|
/**
|
|
* Load component metadata
|
|
*/
|
|
async loadComponentMetadata(componentName) {
|
|
try {
|
|
const response = await fetch(`/playground/api/component/${componentName}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.componentMetadata = data.data;
|
|
} else {
|
|
console.error('Failed to load metadata:', data.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading metadata:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply state from editor
|
|
*/
|
|
async applyState() {
|
|
const jsonText = this.stateEditor.value.trim();
|
|
const validationEl = this.container.querySelector('#state-validation');
|
|
|
|
// Validate JSON
|
|
try {
|
|
const state = jsonText ? JSON.parse(jsonText) : {};
|
|
this.currentState = state;
|
|
|
|
validationEl.innerHTML = '<span class="playground__success">✓ Valid JSON</span>';
|
|
|
|
// Preview component with new state
|
|
await this.previewComponent();
|
|
} catch (error) {
|
|
validationEl.innerHTML = `<span class="playground__error">✗ Invalid JSON: ${error.message}</span>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset state to default
|
|
*/
|
|
resetState() {
|
|
this.currentState = {};
|
|
this.stateEditor.value = '{}';
|
|
this.container.querySelector('#state-validation').innerHTML = '';
|
|
|
|
if (this.selectedComponent) {
|
|
this.previewComponent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format JSON in editor
|
|
*/
|
|
formatJSON() {
|
|
try {
|
|
const json = JSON.parse(this.stateEditor.value || '{}');
|
|
this.stateEditor.value = JSON.stringify(json, null, 2);
|
|
this.container.querySelector('#state-validation').innerHTML = '<span class="playground__success">✓ Formatted</span>';
|
|
} catch (error) {
|
|
this.container.querySelector('#state-validation').innerHTML = `<span class="playground__error">✗ Invalid JSON</span>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preview component with current state
|
|
*/
|
|
async previewComponent() {
|
|
if (!this.selectedComponent) return;
|
|
|
|
this.previewContainer.innerHTML = '<div class="playground__loading">Loading preview...</div>';
|
|
|
|
try {
|
|
const startTime = performance.now();
|
|
|
|
const response = await fetch('/playground/api/preview', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
component_name: this.selectedComponent,
|
|
state: this.currentState,
|
|
instance_id: this.previewInstanceId
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
const endTime = performance.now();
|
|
|
|
if (data.success) {
|
|
this.previewContainer.innerHTML = data.html;
|
|
|
|
// Update metrics
|
|
this.metrics.renderTime = data.render_time_ms;
|
|
this.metrics.stateSize = JSON.stringify(data.state).length;
|
|
this.updateMetrics();
|
|
|
|
// Initialize LiveComponent in preview
|
|
if (window.LiveComponent) {
|
|
window.LiveComponent.initComponent(this.previewContainer.firstElementChild);
|
|
}
|
|
} else {
|
|
this.previewContainer.innerHTML = `
|
|
<div class="playground__error">
|
|
Preview failed: ${data.error}
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (error) {
|
|
this.previewContainer.innerHTML = `
|
|
<div class="playground__error">
|
|
Error: ${error.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render actions for selected component
|
|
*/
|
|
renderActions() {
|
|
if (!this.componentMetadata || !this.componentMetadata.actions) {
|
|
this.actionTester.innerHTML = '<div class="playground__empty">No actions available</div>';
|
|
return;
|
|
}
|
|
|
|
const actions = this.componentMetadata.actions.filter(action =>
|
|
!['onMount', 'onUpdated', 'onDestroy'].includes(action.name)
|
|
);
|
|
|
|
if (actions.length === 0) {
|
|
this.actionTester.innerHTML = '<div class="playground__empty">No actions available</div>';
|
|
return;
|
|
}
|
|
|
|
this.actionTester.innerHTML = actions.map(action => `
|
|
<div class="playground__action">
|
|
<button
|
|
class="playground__button playground__button--action"
|
|
data-action="${action.name}"
|
|
>
|
|
${action.name}()
|
|
</button>
|
|
${action.parameters.length > 0 ? `
|
|
<div class="playground__action-params">
|
|
${action.parameters.map(param => `
|
|
<label>
|
|
${param.name} (${param.type}):
|
|
<input type="text" data-param="${param.name}" placeholder="${param.type}" />
|
|
</label>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`).join('');
|
|
|
|
// Attach action button listeners
|
|
this.actionTester.querySelectorAll('[data-action]').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
this.executeAction(button.dataset.action, button.closest('.playground__action'));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute component action
|
|
*/
|
|
async executeAction(actionName, actionElement) {
|
|
// Get parameters
|
|
const parameters = {};
|
|
if (actionElement) {
|
|
actionElement.querySelectorAll('[data-param]').forEach(input => {
|
|
const paramName = input.dataset.param;
|
|
let value = input.value;
|
|
|
|
// Try to parse as number or boolean
|
|
if (value === 'true') value = true;
|
|
else if (value === 'false') value = false;
|
|
else if (!isNaN(value) && value !== '') value = Number(value);
|
|
|
|
parameters[paramName] = value;
|
|
});
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/playground/api/action', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
component_id: `${this.selectedComponent}:${this.previewInstanceId}`,
|
|
action_name: actionName,
|
|
parameters: parameters,
|
|
current_state: this.currentState
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Update current state
|
|
this.currentState = data.new_state;
|
|
this.stateEditor.value = JSON.stringify(this.currentState, null, 2);
|
|
|
|
// Update preview
|
|
this.previewContainer.innerHTML = data.html;
|
|
|
|
// Update metrics
|
|
this.metrics.actionExecutions++;
|
|
this.updateMetrics();
|
|
|
|
// Re-initialize LiveComponent
|
|
if (window.LiveComponent) {
|
|
window.LiveComponent.initComponent(this.previewContainer.firstElementChild);
|
|
}
|
|
} else {
|
|
alert(`Action failed: ${data.error}`);
|
|
}
|
|
} catch (error) {
|
|
alert(`Error executing action: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update metrics display
|
|
*/
|
|
updateMetrics() {
|
|
this.metricsDisplay.innerHTML = `
|
|
<div class="playground__metrics-grid">
|
|
<div class="playground__metric">
|
|
<span class="playground__metric-label">Render Time</span>
|
|
<span class="playground__metric-value">${this.metrics.renderTime.toFixed(2)}ms</span>
|
|
</div>
|
|
<div class="playground__metric">
|
|
<span class="playground__metric-label">State Size</span>
|
|
<span class="playground__metric-value">${this.metrics.stateSize} bytes</span>
|
|
</div>
|
|
<div class="playground__metric">
|
|
<span class="playground__metric-label">Actions Executed</span>
|
|
<span class="playground__metric-value">${this.metrics.actionExecutions}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Update generated template code
|
|
*/
|
|
updateGeneratedCode() {
|
|
if (!this.selectedComponent) return;
|
|
|
|
const code = `<!-- Use in your template -->\n{{{ ${this.selectedComponent} }}}`;
|
|
|
|
this.container.querySelector('#generated-code code').textContent = code;
|
|
}
|
|
|
|
/**
|
|
* Copy generated code to clipboard
|
|
*/
|
|
async copyCode() {
|
|
const code = this.container.querySelector('#generated-code code').textContent;
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(code);
|
|
const button = this.container.querySelector('#copy-code');
|
|
const originalText = button.textContent;
|
|
button.textContent = '✓ Copied!';
|
|
setTimeout(() => {
|
|
button.textContent = originalText;
|
|
}, 2000);
|
|
} catch (error) {
|
|
alert('Failed to copy code to clipboard');
|
|
}
|
|
}
|
|
}
|
|
|
|
export default ComponentPlayground;
|