feat(dees-button,dees-statsgrid): add unified icon property and icon-position support to dees-button; add partition and disk tile types to dees-statsgrid
This commit is contained in:
@@ -30,12 +30,30 @@ export interface ICpuCore {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface IPartitionData {
|
||||
used: number; // bytes
|
||||
total: number; // bytes
|
||||
filesystem: string; // e.g., 'ext4', 'NTFS', 'btrfs'
|
||||
mountPoint?: string; // e.g., '/', '/home', 'C:'
|
||||
}
|
||||
|
||||
export interface IDiskData {
|
||||
capacity: number; // bytes
|
||||
model?: string; // e.g., 'Samsung 970 EVO Plus'
|
||||
type?: 'ssd' | 'hdd' | 'nvme';
|
||||
iops?: {
|
||||
read: number;
|
||||
write: number;
|
||||
};
|
||||
health?: number; // 0-100 (100 = new, 0 = end of life)
|
||||
}
|
||||
|
||||
export interface IStatsTile {
|
||||
id: string;
|
||||
title: string;
|
||||
value: number | string;
|
||||
unit?: string;
|
||||
type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text' | 'multiPercentage' | 'cpuCores';
|
||||
type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text' | 'multiPercentage' | 'cpuCores' | 'partition' | 'disk';
|
||||
|
||||
// Layout options
|
||||
columnSpan?: number; // Number of columns to span (default: 1)
|
||||
@@ -60,6 +78,12 @@ export interface IStatsTile {
|
||||
// For cpuCores type
|
||||
coresData?: ICpuCore[];
|
||||
|
||||
// For partition type
|
||||
partitionData?: IPartitionData;
|
||||
|
||||
// For disk type
|
||||
diskData?: IDiskData;
|
||||
|
||||
// Visual customization
|
||||
color?: string;
|
||||
icon?: string;
|
||||
@@ -485,6 +509,219 @@ export class DeesStatsGrid extends DeesElement {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Partition Styles */
|
||||
.partition-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partition-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partition-percentage {
|
||||
font-size: var(--value-font-size);
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.partition-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.partition-bar-fill {
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.partition-bar-fill.warning {
|
||||
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
||||
}
|
||||
|
||||
.partition-bar-fill.critical {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
|
||||
}
|
||||
|
||||
.partition-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.partition-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.partition-stat-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.partition-stat-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.partition-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.partition-filesystem {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.partition-mountpoint {
|
||||
font-size: 11px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
}
|
||||
|
||||
/* Disk Styles */
|
||||
.disk-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.disk-capacity {
|
||||
font-size: var(--value-font-size);
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.disk-model {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.disk-type-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.disk-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.disk-iops {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.disk-iops-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.disk-iops-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.disk-iops-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
}
|
||||
|
||||
.disk-health {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.disk-health-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.disk-health-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.disk-health-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
|
||||
}
|
||||
|
||||
.disk-health-bar {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disk-health-fill {
|
||||
height: 100%;
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.disk-health-fill.good {
|
||||
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
}
|
||||
|
||||
.disk-health-fill.warning {
|
||||
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
|
||||
}
|
||||
|
||||
.disk-health-fill.critical {
|
||||
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
|
||||
}
|
||||
|
||||
/* Trend Styles */
|
||||
.trend-container {
|
||||
width: 100%;
|
||||
@@ -659,6 +896,12 @@ export class DeesStatsGrid extends DeesElement {
|
||||
case 'cpuCores':
|
||||
return this.renderCpuCores(tile);
|
||||
|
||||
case 'partition':
|
||||
return this.renderPartition(tile);
|
||||
|
||||
case 'disk':
|
||||
return this.renderDisk(tile);
|
||||
|
||||
case 'text':
|
||||
return html`
|
||||
<div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}">
|
||||
@@ -876,6 +1119,114 @@ export class DeesStatsGrid extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
private renderPartition(tile: IStatsTile): TemplateResult {
|
||||
if (!tile.partitionData) {
|
||||
return html`<div class="tile-value">${tile.value}</div>`;
|
||||
}
|
||||
|
||||
const { used, total, filesystem, mountPoint } = tile.partitionData;
|
||||
const percentage = Math.min(100, Math.max(0, (used / total) * 100));
|
||||
const free = total - used;
|
||||
|
||||
// Determine color class based on usage
|
||||
const getColorClass = (): string => {
|
||||
if (percentage >= 90) return 'critical';
|
||||
if (percentage >= 75) return 'warning';
|
||||
return '';
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="partition-wrapper">
|
||||
<div class="partition-header">
|
||||
<span class="partition-percentage">${Math.round(percentage)}%</span>
|
||||
</div>
|
||||
<div class="partition-bar">
|
||||
<div
|
||||
class="partition-bar-fill ${getColorClass()}"
|
||||
style="width: ${percentage}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="partition-stats">
|
||||
<div class="partition-stat">
|
||||
<span class="partition-stat-label">Used</span>
|
||||
<span class="partition-stat-value">${this.formatBytes(used)}</span>
|
||||
</div>
|
||||
<div class="partition-stat">
|
||||
<span class="partition-stat-label">Free</span>
|
||||
<span class="partition-stat-value">${this.formatBytes(free)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="partition-meta">
|
||||
<span class="partition-filesystem">${filesystem}</span>
|
||||
${mountPoint ? html`<span class="partition-mountpoint">${mountPoint}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDisk(tile: IStatsTile): TemplateResult {
|
||||
if (!tile.diskData) {
|
||||
return html`<div class="tile-value">${tile.value}</div>`;
|
||||
}
|
||||
|
||||
const { capacity, model, type, iops, health } = tile.diskData;
|
||||
|
||||
// Determine health color class (inverted - high is good)
|
||||
const getHealthClass = (value: number): string => {
|
||||
if (value >= 70) return 'good';
|
||||
if (value >= 30) return 'warning';
|
||||
return 'critical';
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="disk-wrapper">
|
||||
<div class="disk-capacity">${this.formatBytes(capacity)}</div>
|
||||
${model || type ? html`
|
||||
<div class="disk-model">
|
||||
${model ? html`<span>${model}</span>` : ''}
|
||||
${type ? html`<span class="disk-type-badge">${type}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="disk-metrics">
|
||||
${iops ? html`
|
||||
<div class="disk-iops">
|
||||
<div class="disk-iops-item">
|
||||
<span class="disk-iops-label">Read</span>
|
||||
<span class="disk-iops-value">${iops.read.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="disk-iops-item">
|
||||
<span class="disk-iops-label">Write</span>
|
||||
<span class="disk-iops-value">${iops.write.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${health !== undefined ? html`
|
||||
<div class="disk-health">
|
||||
<div class="disk-health-header">
|
||||
<span class="disk-health-label">Health</span>
|
||||
<span class="disk-health-value">${health}%</span>
|
||||
</div>
|
||||
<div class="disk-health-bar">
|
||||
<div
|
||||
class="disk-health-fill ${getHealthClass(health)}"
|
||||
style="width: ${health}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async handleGridAction(action: plugins.tsclass.website.IMenuItem) {
|
||||
if (action.action) {
|
||||
await action.action();
|
||||
|
||||
Reference in New Issue
Block a user