Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
286 lines
8.8 KiB
JavaScript
286 lines
8.8 KiB
JavaScript
/**
|
|
* Integrations Page - MCP Integration Configuration
|
|
*
|
|
* Allows users to configure project integrations:
|
|
* - Figma (design tokens, components)
|
|
* - Jira (issue tracking)
|
|
* - Confluence (documentation)
|
|
*/
|
|
|
|
import claudeService from '../services/claude-service.js';
|
|
import logger from '../core/logger.js';
|
|
|
|
class IntegrationsPage {
|
|
constructor() {
|
|
this.container = null;
|
|
this.projectId = null;
|
|
this.integrations = [];
|
|
this.healthStatus = [];
|
|
}
|
|
|
|
/**
|
|
* Initialize the page
|
|
*/
|
|
async init(container, projectId) {
|
|
this.container = container;
|
|
this.projectId = projectId;
|
|
|
|
claudeService.setProject(projectId);
|
|
|
|
await this.loadData();
|
|
this.render();
|
|
}
|
|
|
|
/**
|
|
* Load integrations data
|
|
*/
|
|
async loadData() {
|
|
try {
|
|
// Load global health status
|
|
this.healthStatus = await claudeService.getIntegrations();
|
|
|
|
// Load project-specific integrations
|
|
if (this.projectId) {
|
|
this.integrations = await claudeService.getProjectIntegrations(this.projectId);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Integrations', 'Failed to load data', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the page
|
|
*/
|
|
render() {
|
|
if (!this.container) return;
|
|
|
|
this.container.innerHTML = `
|
|
<div class="integrations-page">
|
|
<header class="page-header">
|
|
<h1>MCP Integrations</h1>
|
|
<p class="subtitle">Configure external service connections for Claude</p>
|
|
</header>
|
|
|
|
<div class="integrations-grid">
|
|
${this.renderIntegrationCard('figma', {
|
|
title: 'Figma',
|
|
description: 'Connect to Figma for design tokens, components, and styles',
|
|
icon: '<svg viewBox="0 0 24 24" width="32" height="32"><path fill="currentColor" d="M12 12a4 4 0 1 1 4 4H8a4 4 0 1 1 0-8V4h4a4 4 0 0 1 0 8z"/></svg>',
|
|
fields: [
|
|
{ name: 'api_token', label: 'API Token', type: 'password', placeholder: 'Enter Figma API token' }
|
|
]
|
|
})}
|
|
|
|
${this.renderIntegrationCard('jira', {
|
|
title: 'Jira',
|
|
description: 'Connect to Jira for issue tracking and project management',
|
|
icon: '<svg viewBox="0 0 24 24" width="32" height="32"><path fill="currentColor" d="M12 2L4 10l8 8 8-8-8-8zm0 4l4 4-4 4-4-4 4-4z"/></svg>',
|
|
fields: [
|
|
{ name: 'url', label: 'Jira URL', type: 'url', placeholder: 'https://your-domain.atlassian.net' },
|
|
{ name: 'username', label: 'Email', type: 'email', placeholder: 'your-email@example.com' },
|
|
{ name: 'api_token', label: 'API Token', type: 'password', placeholder: 'Enter Jira API token' }
|
|
]
|
|
})}
|
|
|
|
${this.renderIntegrationCard('confluence', {
|
|
title: 'Confluence',
|
|
description: 'Connect to Confluence for documentation and knowledge base',
|
|
icon: '<svg viewBox="0 0 24 24" width="32" height="32"><path fill="currentColor" d="M4 4h16v16H4V4zm2 2v12h12V6H6z"/></svg>',
|
|
fields: [
|
|
{ name: 'url', label: 'Confluence URL', type: 'url', placeholder: 'https://your-domain.atlassian.net/wiki' },
|
|
{ name: 'username', label: 'Email', type: 'email', placeholder: 'your-email@example.com' },
|
|
{ name: 'api_token', label: 'API Token', type: 'password', placeholder: 'Enter Confluence API token' }
|
|
]
|
|
})}
|
|
</div>
|
|
|
|
<section class="mcp-tools-section">
|
|
<h2>Available MCP Tools</h2>
|
|
<div class="tools-list" id="tools-list">
|
|
<p class="loading">Loading tools...</p>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
`;
|
|
|
|
this.attachEventListeners();
|
|
this.loadToolsList();
|
|
}
|
|
|
|
/**
|
|
* Render an integration card
|
|
*/
|
|
renderIntegrationCard(type, config) {
|
|
const health = this.healthStatus.find(h => h.integration_type === type) || {};
|
|
const projectConfig = this.integrations.find(i => i.integration_type === type);
|
|
const isConfigured = !!projectConfig;
|
|
const isEnabled = projectConfig?.enabled ?? false;
|
|
|
|
const healthClass = health.is_healthy ? 'healthy' : 'unhealthy';
|
|
const healthText = health.is_healthy ? 'Healthy' : `Unhealthy (${health.failure_count} failures)`;
|
|
|
|
return `
|
|
<div class="integration-card ${isEnabled ? 'enabled' : ''}" data-type="${type}">
|
|
<div class="card-header">
|
|
<div class="icon">${config.icon}</div>
|
|
<div class="info">
|
|
<h3>${config.title}</h3>
|
|
<p>${config.description}</p>
|
|
</div>
|
|
<div class="status">
|
|
<span class="health-badge ${healthClass}">${healthText}</span>
|
|
${isConfigured ? '<span class="configured-badge">Configured</span>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<form class="integration-form" data-type="${type}">
|
|
${config.fields.map(field => `
|
|
<div class="form-group">
|
|
<label for="${type}-${field.name}">${field.label}</label>
|
|
<input
|
|
type="${field.type}"
|
|
id="${type}-${field.name}"
|
|
name="${field.name}"
|
|
placeholder="${field.placeholder}"
|
|
${field.type === 'password' ? 'autocomplete="off"' : ''}
|
|
/>
|
|
</div>
|
|
`).join('')}
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">
|
|
${isConfigured ? 'Update' : 'Connect'}
|
|
</button>
|
|
${isConfigured ? `
|
|
<button type="button" class="btn btn-secondary toggle-btn" data-enabled="${isEnabled}">
|
|
${isEnabled ? 'Disable' : 'Enable'}
|
|
</button>
|
|
<button type="button" class="btn btn-danger delete-btn">Disconnect</button>
|
|
` : ''}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Attach event listeners
|
|
*/
|
|
attachEventListeners() {
|
|
// Form submissions
|
|
this.container.querySelectorAll('.integration-form').forEach(form => {
|
|
form.addEventListener('submit', (e) => this.handleFormSubmit(e));
|
|
});
|
|
|
|
// Toggle buttons
|
|
this.container.querySelectorAll('.toggle-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => this.handleToggle(e));
|
|
});
|
|
|
|
// Delete buttons
|
|
this.container.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => this.handleDelete(e));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle form submission
|
|
*/
|
|
async handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const type = form.dataset.type;
|
|
const formData = new FormData(form);
|
|
const config = {};
|
|
|
|
formData.forEach((value, key) => {
|
|
if (value) config[key] = value;
|
|
});
|
|
|
|
if (Object.keys(config).length === 0) {
|
|
alert('Please fill in at least one field');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await claudeService.configureIntegration(type, config, this.projectId);
|
|
alert(`${type} integration configured successfully!`);
|
|
await this.loadData();
|
|
this.render();
|
|
} catch (error) {
|
|
alert(`Failed to configure ${type}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle toggle
|
|
*/
|
|
async handleToggle(e) {
|
|
const card = e.target.closest('.integration-card');
|
|
const type = card.dataset.type;
|
|
const currentlyEnabled = e.target.dataset.enabled === 'true';
|
|
|
|
try {
|
|
await claudeService.toggleIntegration(type, !currentlyEnabled, this.projectId);
|
|
await this.loadData();
|
|
this.render();
|
|
} catch (error) {
|
|
alert(`Failed to toggle ${type}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle delete
|
|
*/
|
|
async handleDelete(e) {
|
|
const card = e.target.closest('.integration-card');
|
|
const type = card.dataset.type;
|
|
|
|
if (!confirm(`Are you sure you want to disconnect ${type}?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await claudeService.deleteIntegration(type, this.projectId);
|
|
await this.loadData();
|
|
this.render();
|
|
} catch (error) {
|
|
alert(`Failed to disconnect ${type}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load and display MCP tools list
|
|
*/
|
|
async loadToolsList() {
|
|
const toolsList = this.container.querySelector('#tools-list');
|
|
|
|
try {
|
|
const result = await claudeService.getMcpTools();
|
|
|
|
const html = Object.entries(result.tools).map(([category, tools]) => `
|
|
<div class="tool-category">
|
|
<h3>${category.charAt(0).toUpperCase() + category.slice(1)} Tools (${tools.length})</h3>
|
|
<ul>
|
|
${tools.map(tool => `
|
|
<li>
|
|
<strong>${tool.name}</strong>
|
|
<span>${tool.description}</span>
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
</div>
|
|
`).join('');
|
|
|
|
toolsList.innerHTML = html || '<p>No tools available</p>';
|
|
} catch (error) {
|
|
toolsList.innerHTML = '<p class="error">Failed to load tools</p>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton
|
|
const integrationsPage = new IntegrationsPage();
|
|
export default integrationsPage;
|
|
export { IntegrationsPage };
|