Revert "feat: Enterprise DSS architecture implementation"
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
This reverts commit 9dbd56271e.
This commit is contained in:
@@ -1,424 +0,0 @@
|
||||
import { JSX } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import { Card, CardHeader, CardContent } from './base/Card';
|
||||
import { Badge } from './base/Badge';
|
||||
import { Button } from './base/Button';
|
||||
import { Spinner } from './base/Spinner';
|
||||
|
||||
interface ProjectMetrics {
|
||||
project: string;
|
||||
total_files: number;
|
||||
passed_files: number;
|
||||
failed_files: number;
|
||||
total_errors: number;
|
||||
total_warnings: number;
|
||||
rules_version: string;
|
||||
last_updated: string;
|
||||
adoption_score: number;
|
||||
}
|
||||
|
||||
interface PortfolioData {
|
||||
total_projects: number;
|
||||
projects_passing: number;
|
||||
projects_failing: number;
|
||||
total_errors: number;
|
||||
total_warnings: number;
|
||||
average_adoption_score: number;
|
||||
projects: ProjectMetrics[];
|
||||
}
|
||||
|
||||
interface ViolationLocation {
|
||||
file: string;
|
||||
rule: string;
|
||||
line: number;
|
||||
column: number;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
interface TrendDataPoint {
|
||||
date: string;
|
||||
errors: number;
|
||||
warnings: number;
|
||||
pass_rate: number;
|
||||
}
|
||||
|
||||
const API_BASE = '/api/metrics';
|
||||
|
||||
export function PortfolioDashboard(): JSX.Element {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [portfolio, setPortfolio] = useState<PortfolioData | null>(null);
|
||||
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
||||
const [projectDetails, setProjectDetails] = useState<any>(null);
|
||||
const [trends, setTrends] = useState<TrendDataPoint[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadPortfolio();
|
||||
loadTrends();
|
||||
}, []);
|
||||
|
||||
async function loadPortfolio() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/portfolio?days=30`);
|
||||
if (!response.ok) throw new Error('Failed to load portfolio data');
|
||||
const data = await response.json();
|
||||
setPortfolio(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTrends() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/trends?days=30`);
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
setTrends(data.data || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load trends:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectDetails(projectName: string) {
|
||||
setSelectedProject(projectName);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/projects/${encodeURIComponent(projectName)}`);
|
||||
if (!response.ok) throw new Error('Failed to load project details');
|
||||
const data = await response.json();
|
||||
setProjectDetails(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load project details:', err);
|
||||
setProjectDetails(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="portfolio-dashboard portfolio-loading">
|
||||
<Spinner size="lg" />
|
||||
<span>Loading portfolio metrics...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="portfolio-dashboard portfolio-error">
|
||||
<Card variant="bordered" padding="lg">
|
||||
<CardContent>
|
||||
<div className="error-message">
|
||||
<Badge variant="error">Error</Badge>
|
||||
<p>{error}</p>
|
||||
<Button variant="outline" onClick={loadPortfolio}>Retry</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!portfolio) {
|
||||
return (
|
||||
<div className="portfolio-dashboard">
|
||||
<Card variant="bordered" padding="lg">
|
||||
<CardContent>
|
||||
<p className="text-muted">No portfolio data available. Run DSS validation in your CI pipelines to collect metrics.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="portfolio-dashboard">
|
||||
<div className="portfolio-header">
|
||||
<h1>Design System Portfolio</h1>
|
||||
<p className="subtitle">Adoption metrics across {portfolio.total_projects} projects</p>
|
||||
<Button variant="ghost" size="sm" onClick={loadPortfolio}>Refresh</Button>
|
||||
</div>
|
||||
|
||||
{/* Portfolio Summary */}
|
||||
<div className="metrics-grid portfolio-metrics">
|
||||
<MetricCard
|
||||
label="Total Projects"
|
||||
value={portfolio.total_projects}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Passing"
|
||||
value={portfolio.projects_passing}
|
||||
variant="success"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Failing"
|
||||
value={portfolio.projects_failing}
|
||||
variant={portfolio.projects_failing > 0 ? 'error' : 'default'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Adoption Score"
|
||||
value={`${portfolio.average_adoption_score}%`}
|
||||
variant={portfolio.average_adoption_score >= 80 ? 'success' : portfolio.average_adoption_score >= 60 ? 'warning' : 'error'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Errors"
|
||||
value={portfolio.total_errors}
|
||||
variant={portfolio.total_errors > 0 ? 'error' : 'success'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Total Warnings"
|
||||
value={portfolio.total_warnings}
|
||||
variant={portfolio.total_warnings > 0 ? 'warning' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Trend Chart (simple text-based for now) */}
|
||||
{trends.length > 0 && (
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader title="30-Day Trend" subtitle="Errors and warnings over time" />
|
||||
<CardContent>
|
||||
<div className="trend-chart">
|
||||
{trends.slice(-7).map((point, idx) => (
|
||||
<div key={idx} className="trend-bar">
|
||||
<div className="trend-date">{point.date.slice(5)}</div>
|
||||
<div className="trend-values">
|
||||
<span className="trend-errors" title={`${point.errors} errors`}>
|
||||
{point.errors > 0 && '●'.repeat(Math.min(point.errors, 10))}
|
||||
</span>
|
||||
<span className="trend-warnings" title={`${point.warnings} warnings`}>
|
||||
{point.warnings > 0 && '○'.repeat(Math.min(Math.ceil(point.warnings / 10), 10))}
|
||||
</span>
|
||||
</div>
|
||||
<div className="trend-rate">{point.pass_rate}%</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Projects Table */}
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader
|
||||
title="Projects"
|
||||
subtitle="Click a project to see violation details"
|
||||
/>
|
||||
<CardContent>
|
||||
<div className="projects-table">
|
||||
<div className="table-header">
|
||||
<span className="col-project">Project</span>
|
||||
<span className="col-score">Score</span>
|
||||
<span className="col-files">Files</span>
|
||||
<span className="col-errors">Errors</span>
|
||||
<span className="col-warnings">Warnings</span>
|
||||
<span className="col-version">Rules</span>
|
||||
<span className="col-updated">Updated</span>
|
||||
</div>
|
||||
{portfolio.projects.map(project => (
|
||||
<div
|
||||
key={project.project}
|
||||
className={`table-row ${selectedProject === project.project ? 'selected' : ''}`}
|
||||
onClick={() => loadProjectDetails(project.project)}
|
||||
>
|
||||
<span className="col-project">{formatProjectName(project.project)}</span>
|
||||
<span className="col-score">
|
||||
<Badge
|
||||
variant={project.adoption_score >= 80 ? 'success' : project.adoption_score >= 60 ? 'warning' : 'error'}
|
||||
size="sm"
|
||||
>
|
||||
{project.adoption_score.toFixed(0)}%
|
||||
</Badge>
|
||||
</span>
|
||||
<span className="col-files">{project.passed_files}/{project.total_files}</span>
|
||||
<span className="col-errors">
|
||||
{project.total_errors > 0 ? (
|
||||
<Badge variant="error" size="sm">{project.total_errors}</Badge>
|
||||
) : (
|
||||
<span className="text-success">0</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="col-warnings">
|
||||
{project.total_warnings > 0 ? (
|
||||
<Badge variant="warning" size="sm">{project.total_warnings}</Badge>
|
||||
) : (
|
||||
<span className="text-muted">0</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="col-version">{project.rules_version}</span>
|
||||
<span className="col-updated">{formatTimeAgo(project.last_updated)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Project Details Panel */}
|
||||
{selectedProject && projectDetails && (
|
||||
<ProjectDetailsPanel
|
||||
project={selectedProject}
|
||||
details={projectDetails}
|
||||
onClose={() => setSelectedProject(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface MetricCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
variant?: 'default' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
function MetricCard({ label, value, variant = 'default' }: MetricCardProps): JSX.Element {
|
||||
return (
|
||||
<Card variant="bordered" padding="md">
|
||||
<div className={`metric-display metric-${variant}`}>
|
||||
<span className="metric-label">{label}</span>
|
||||
<span className="metric-value">{value}</span>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProjectDetailsPanelProps {
|
||||
project: string;
|
||||
details: any;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ProjectDetailsPanel({ project, details, onClose }: ProjectDetailsPanelProps): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'violations' | 'history'>('overview');
|
||||
|
||||
return (
|
||||
<Card variant="elevated" padding="lg" className="project-details-panel">
|
||||
<CardHeader
|
||||
title={formatProjectName(project)}
|
||||
subtitle="Detailed metrics and violations"
|
||||
action={<Button variant="ghost" size="sm" onClick={onClose}>×</Button>}
|
||||
/>
|
||||
<CardContent>
|
||||
{/* Tabs */}
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab ${activeTab === 'overview' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('overview')}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === 'violations' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('violations')}
|
||||
>
|
||||
Violations
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === 'history' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('history')}
|
||||
>
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'overview' && details.latest && (
|
||||
<div className="tab-content">
|
||||
<div className="detail-grid">
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Branch</span>
|
||||
<span className="detail-value">{details.latest.branch}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Commit</span>
|
||||
<span className="detail-value">{details.latest.commit?.slice(0, 7)}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Rules Version</span>
|
||||
<span className="detail-value">{details.latest.rules_version}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<span className="detail-label">Adoption Score</span>
|
||||
<span className="detail-value">{details.latest.adoption_score?.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Violations by Rule */}
|
||||
{details.violations_by_rule && Object.keys(details.violations_by_rule).length > 0 && (
|
||||
<div className="violations-breakdown">
|
||||
<h4>Violations by Rule</h4>
|
||||
{Object.entries(details.violations_by_rule).map(([rule, count]) => (
|
||||
<div key={rule} className="rule-row">
|
||||
<span className="rule-name">{rule}</span>
|
||||
<Badge variant="error" size="sm">{count as number}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'violations' && (
|
||||
<div className="tab-content violations-list">
|
||||
{details.violation_locations?.length === 0 ? (
|
||||
<p className="text-muted">No violations found</p>
|
||||
) : (
|
||||
details.violation_locations?.map((v: ViolationLocation, idx: number) => (
|
||||
<div key={idx} className="violation-item">
|
||||
<div className="violation-location">
|
||||
<code>{v.file}:{v.line}:{v.column}</code>
|
||||
</div>
|
||||
<div className="violation-rule">
|
||||
<Badge variant={v.severity === 'error' ? 'error' : 'warning'} size="sm">
|
||||
{v.rule}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && (
|
||||
<div className="tab-content history-list">
|
||||
{details.history?.map((h: any, idx: number) => (
|
||||
<div key={idx} className="history-item">
|
||||
<span className="history-commit">{h.commit?.slice(0, 7)}</span>
|
||||
<span className="history-branch">{h.branch}</span>
|
||||
<span className="history-errors">
|
||||
{h.errors > 0 ? <Badge variant="error" size="sm">{h.errors}</Badge> : '0'}
|
||||
</span>
|
||||
<span className="history-time">{formatTimeAgo(h.timestamp)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function formatProjectName(name: string): string {
|
||||
// Format "org/repo" or just "repo"
|
||||
const parts = name.split('/');
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
function formatTimeAgo(timestamp: string): string {
|
||||
if (!timestamp) return 'Unknown';
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 1) return 'Just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
|
||||
export default PortfolioDashboard;
|
||||
@@ -1,360 +0,0 @@
|
||||
/* Portfolio Dashboard Styles */
|
||||
|
||||
.portfolio-dashboard {
|
||||
padding: var(--spacing-lg);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.portfolio-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.portfolio-header h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.portfolio-header .subtitle {
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.portfolio-loading,
|
||||
.portfolio-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.portfolio-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.metric-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-success .metric-value {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.metric-warning .metric-value {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.metric-error .metric-value {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
/* Trend Chart */
|
||||
.trend-chart {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
overflow-x: auto;
|
||||
padding: var(--spacing-sm) 0;
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 80px;
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.trend-date {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.trend-values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.trend-errors {
|
||||
color: var(--color-error);
|
||||
font-size: 10px;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.trend-warnings {
|
||||
color: var(--color-warning);
|
||||
font-size: 8px;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.trend-rate {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* Projects Table */
|
||||
.projects-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.table-header,
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 80px 80px 80px 80px 80px 100px;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.table-row.selected {
|
||||
background: var(--color-bg-tertiary);
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.col-project {
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-score,
|
||||
.col-files,
|
||||
.col-errors,
|
||||
.col-warnings,
|
||||
.col-version,
|
||||
.col-updated {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.col-updated {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Project Details Panel */
|
||||
.project-details-panel {
|
||||
margin-top: var(--spacing-lg);
|
||||
animation: slideUp 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--color-primary);
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.violations-breakdown {
|
||||
margin-top: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.violations-breakdown h4 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.rule-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs) 0;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.rule-name {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* Violations List */
|
||||
.violations-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.violation-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.violation-location code {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* History List */
|
||||
.history-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 100px 60px 1fr;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) 0;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.history-commit {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.history-branch {
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
text-align: right;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Text utilities */
|
||||
.text-success {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.table-header,
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 60px 60px 60px;
|
||||
}
|
||||
|
||||
.col-version,
|
||||
.col-updated,
|
||||
.col-warnings {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portfolio-metrics {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,8 @@ import { Input, Select } from '../components/base/Input';
|
||||
import { Spinner } from '../components/base/Spinner';
|
||||
import { endpoints } from '../api/client';
|
||||
import { currentProject } from '../state/project';
|
||||
import { PortfolioDashboard } from '../components/PortfolioDashboard';
|
||||
import type { TokenDrift, Component, FigmaExtractResult } from '../api/types';
|
||||
import './Workdesk.css';
|
||||
import '../styles/portfolio.css';
|
||||
|
||||
interface UIWorkdeskProps {
|
||||
activeTool: string | null;
|
||||
@@ -28,7 +26,6 @@ export default function UIWorkdesk({ activeTool }: UIWorkdeskProps) {
|
||||
'code-generator': <CodeGeneratorTool />,
|
||||
'quick-wins': <QuickWinsTool />,
|
||||
'token-drift': <TokenDriftTool />,
|
||||
'portfolio': <PortfolioDashboard />,
|
||||
};
|
||||
|
||||
return toolViews[activeTool] || <ToolPlaceholder name={activeTool} />;
|
||||
|
||||
Reference in New Issue
Block a user