419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as appstate from '../appstate.js';
|
|
import * as shared from './shared/index.js';
|
|
|
|
import {
|
|
css,
|
|
cssManager,
|
|
customElement,
|
|
DeesElement,
|
|
html,
|
|
state,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
|
|
|
@customElement('objst-view-config')
|
|
export class ObjstViewConfig extends DeesElement {
|
|
@state()
|
|
accessor configState: appstate.IConfigState = { config: null, clusterHealth: null };
|
|
|
|
constructor() {
|
|
super();
|
|
const sub = appstate.configStatePart
|
|
.select((s) => s)
|
|
.subscribe((configState) => {
|
|
this.configState = configState;
|
|
});
|
|
this.rxSubscriptions.push(sub);
|
|
}
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
shared.viewHostCss,
|
|
css`
|
|
.sectionSpacer {
|
|
margin-top: 32px;
|
|
}
|
|
.infoPanel {
|
|
margin-top: 32px;
|
|
padding: 24px;
|
|
border-radius: 8px;
|
|
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
|
|
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a4a')};
|
|
}
|
|
.infoPanel h2 {
|
|
margin: 0 0 16px 0;
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
|
}
|
|
.infoPanel p {
|
|
margin: 0 0 16px 0;
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
line-height: 1.5;
|
|
}
|
|
.infoPanel .row {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
}
|
|
.infoPanel .label {
|
|
min-width: 260px;
|
|
font-family: monospace;
|
|
font-weight: 500;
|
|
color: ${cssManager.bdTheme('#1565c0', '#64b5f6')};
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a237e30')};
|
|
}
|
|
.infoPanel .value {
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
margin-left: 12px;
|
|
}
|
|
.driveList {
|
|
margin-top: 16px;
|
|
}
|
|
.driveList .driveItem {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
}
|
|
.driveList .driveIndex {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#e8eaf6', '#1a237e40')};
|
|
color: ${cssManager.bdTheme('#3f51b5', '#7986cb')};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 13px;
|
|
margin-right: 12px;
|
|
}
|
|
.driveList .drivePath {
|
|
font-family: monospace;
|
|
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
background: ${cssManager.bdTheme('#e8e8e8', '#252540')};
|
|
}
|
|
.driveList .driveStatus {
|
|
margin-left: 12px;
|
|
padding: 4px 8px;
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#1b5e20', '#a5d6a7')};
|
|
background: ${cssManager.bdTheme('#e8f5e9', '#1b5e2030')};
|
|
}
|
|
.driveList .driveMeta {
|
|
margin-left: 12px;
|
|
color: ${cssManager.bdTheme('#666', '#999')};
|
|
font-size: 13px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
const config = this.configState.config;
|
|
const clusterHealth = this.configState.clusterHealth;
|
|
|
|
const serverTiles: IStatsTile[] = [
|
|
{
|
|
id: 'objstPort',
|
|
title: 'Storage API Port',
|
|
value: config?.objstPort ?? '--',
|
|
type: 'number',
|
|
icon: 'lucide:network',
|
|
color: '#2196f3',
|
|
},
|
|
{
|
|
id: 'uiPort',
|
|
title: 'UI Port',
|
|
value: config?.uiPort ?? '--',
|
|
type: 'number',
|
|
icon: 'lucide:monitor',
|
|
color: '#00bcd4',
|
|
},
|
|
{
|
|
id: 'region',
|
|
title: 'Region',
|
|
value: config?.region ?? '--',
|
|
type: 'text',
|
|
icon: 'lucide:globe',
|
|
color: '#607d8b',
|
|
},
|
|
{
|
|
id: 'storageDir',
|
|
title: 'Storage Directory',
|
|
value: config?.storageDirectory ?? '--',
|
|
type: 'text',
|
|
icon: 'lucide:hardDrive',
|
|
color: '#9c27b0',
|
|
},
|
|
{
|
|
id: 'auth',
|
|
title: 'Authentication',
|
|
value: config?.authEnabled ? 'Enabled' : 'Disabled',
|
|
type: 'text',
|
|
icon: 'lucide:shield',
|
|
color: config?.authEnabled ? '#4caf50' : '#f44336',
|
|
},
|
|
{
|
|
id: 'cors',
|
|
title: 'CORS',
|
|
value: config?.corsEnabled ? 'Enabled' : 'Disabled',
|
|
type: 'text',
|
|
icon: 'lucide:globe2',
|
|
color: config?.corsEnabled ? '#4caf50' : '#ff9800',
|
|
},
|
|
];
|
|
|
|
const clusterEnabled = clusterHealth?.enabled ?? config?.clusterEnabled ?? false;
|
|
const clusterTiles: IStatsTile[] = [
|
|
{
|
|
id: 'clusterStatus',
|
|
title: 'Cluster Status',
|
|
value: clusterEnabled ? 'Enabled' : 'Disabled',
|
|
type: 'text',
|
|
icon: 'lucide:network',
|
|
color: clusterEnabled ? '#4caf50' : '#ff9800',
|
|
},
|
|
{
|
|
id: 'nodeId',
|
|
title: 'Node ID',
|
|
value: clusterHealth?.nodeId || config?.clusterNodeId || '(auto)',
|
|
type: 'text',
|
|
icon: 'lucide:fingerprint',
|
|
color: '#607d8b',
|
|
},
|
|
{
|
|
id: 'quicPort',
|
|
title: 'QUIC Port',
|
|
value: config?.clusterQuicPort ?? 4433,
|
|
type: 'number',
|
|
icon: 'lucide:radio',
|
|
color: '#00bcd4',
|
|
},
|
|
{
|
|
id: 'seedNodes',
|
|
title: 'Seed Nodes',
|
|
value: config?.clusterSeedNodes?.length ?? 0,
|
|
type: 'number',
|
|
icon: 'lucide:gitBranch',
|
|
color: '#3f51b5',
|
|
description: config?.clusterSeedNodes?.length
|
|
? config.clusterSeedNodes.join(', ')
|
|
: 'No seed nodes configured',
|
|
},
|
|
{
|
|
id: 'heartbeatInterval',
|
|
title: 'Heartbeat Interval',
|
|
value: `${config?.clusterHeartbeatIntervalMs ?? 5000}ms`,
|
|
type: 'text',
|
|
icon: 'lucide:heartPulse',
|
|
color: '#e91e63',
|
|
},
|
|
{
|
|
id: 'heartbeatTimeout',
|
|
title: 'Heartbeat Timeout',
|
|
value: `${config?.clusterHeartbeatTimeoutMs ?? 30000}ms`,
|
|
type: 'text',
|
|
icon: 'lucide:timer',
|
|
color: '#ff5722',
|
|
},
|
|
{
|
|
id: 'quorum',
|
|
title: 'Quorum',
|
|
value: clusterHealth?.enabled
|
|
? clusterHealth.quorumHealthy ? 'Healthy' : 'Degraded'
|
|
: 'Standalone',
|
|
type: 'text',
|
|
icon: 'lucide:activity',
|
|
color: clusterHealth?.quorumHealthy ? '#4caf50' : '#ff9800',
|
|
},
|
|
{
|
|
id: 'peers',
|
|
title: 'Peers',
|
|
value: clusterHealth?.peers?.length ?? 0,
|
|
type: 'number',
|
|
icon: 'lucide:share2',
|
|
color: '#3f51b5',
|
|
},
|
|
];
|
|
|
|
const erasureTiles: IStatsTile[] = [
|
|
{
|
|
id: 'dataShards',
|
|
title: 'Data Shards',
|
|
value: clusterHealth?.erasure?.dataShards ?? config?.erasureDataShards ?? 4,
|
|
type: 'number',
|
|
icon: 'lucide:layers',
|
|
color: '#2196f3',
|
|
},
|
|
{
|
|
id: 'parityShards',
|
|
title: 'Parity Shards',
|
|
value: clusterHealth?.erasure?.parityShards ?? config?.erasureParityShards ?? 2,
|
|
type: 'number',
|
|
icon: 'lucide:shieldCheck',
|
|
color: '#4caf50',
|
|
},
|
|
{
|
|
id: 'chunkSize',
|
|
title: 'Chunk Size',
|
|
value: this.formatBytes(
|
|
clusterHealth?.erasure?.chunkSizeBytes ?? config?.erasureChunkSizeBytes ?? 4194304,
|
|
),
|
|
type: 'text',
|
|
icon: 'lucide:puzzle',
|
|
color: '#9c27b0',
|
|
description: `${clusterHealth?.erasure?.dataShards ?? config?.erasureDataShards ?? 4}+${
|
|
clusterHealth?.erasure?.parityShards ?? config?.erasureParityShards ?? 2
|
|
}`,
|
|
},
|
|
];
|
|
|
|
const drivePaths = clusterHealth?.drives?.length
|
|
? clusterHealth.drives.map((drive) => drive.path)
|
|
: config?.drivePaths?.length
|
|
? config.drivePaths
|
|
: config?.storageDirectory
|
|
? [config.storageDirectory]
|
|
: ['/data'];
|
|
|
|
const driveTiles: IStatsTile[] = [
|
|
{
|
|
id: 'driveCount',
|
|
title: 'Drive Count',
|
|
value: clusterHealth?.drives?.length ?? drivePaths.length,
|
|
type: 'number',
|
|
icon: 'lucide:hardDrive',
|
|
color: '#3f51b5',
|
|
},
|
|
];
|
|
|
|
const refreshAction = {
|
|
name: 'Refresh',
|
|
iconName: 'lucide:refreshCw',
|
|
action: async () => {
|
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
|
|
},
|
|
};
|
|
|
|
return html`
|
|
<objst-sectionheading>Server Configuration</objst-sectionheading>
|
|
<dees-statsgrid
|
|
.tiles="${serverTiles}"
|
|
.gridActions="${[refreshAction]}"
|
|
></dees-statsgrid>
|
|
|
|
<div class="sectionSpacer">
|
|
<objst-sectionheading>Cluster Configuration</objst-sectionheading>
|
|
</div>
|
|
<dees-statsgrid .tiles="${clusterTiles}"></dees-statsgrid>
|
|
|
|
${clusterEnabled
|
|
? html`
|
|
<div class="sectionSpacer">
|
|
<objst-sectionheading>Erasure Coding</objst-sectionheading>
|
|
</div>
|
|
<dees-statsgrid .tiles="${erasureTiles}"></dees-statsgrid>
|
|
`
|
|
: ''}
|
|
|
|
<div class="sectionSpacer">
|
|
<objst-sectionheading>Storage Drives</objst-sectionheading>
|
|
</div>
|
|
<dees-statsgrid .tiles="${driveTiles}"></dees-statsgrid>
|
|
<div class="driveList">
|
|
${(clusterHealth?.drives?.length
|
|
? clusterHealth.drives
|
|
: drivePaths.map((path, index) => ({ path, index, status: 'configured' }))).map((
|
|
drive,
|
|
i,
|
|
) =>
|
|
html`
|
|
<div class="driveItem">
|
|
<div class="driveIndex">${i + 1}</div>
|
|
<span class="drivePath">${drive.path}</span>
|
|
<span class="driveStatus">${drive.status}</span>
|
|
${drive.usedBytes !== undefined && drive.totalBytes !== undefined
|
|
? html`
|
|
<span class="driveMeta">${this.formatBytes(drive.usedBytes)} / ${this
|
|
.formatBytes(drive.totalBytes)}</span>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`
|
|
)}
|
|
</div>
|
|
|
|
<div class="infoPanel">
|
|
<h2>Configuration Reference</h2>
|
|
<p>
|
|
Cluster and drive settings are applied at server startup. To change them, set the environment
|
|
variables and restart the server.
|
|
</p>
|
|
<div class="row">
|
|
<span class="label">OBJST_CLUSTER_ENABLED</span><span class="value"
|
|
>Enable clustering (true/false)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_CLUSTER_NODE_ID</span><span class="value"
|
|
>Unique node identifier</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_CLUSTER_QUIC_PORT</span><span class="value"
|
|
>QUIC transport port (default: 4433)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_CLUSTER_SEED_NODES</span><span class="value"
|
|
>Comma-separated seed node addresses</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_DRIVE_PATHS</span><span class="value"
|
|
>Comma-separated drive mount paths</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_ERASURE_DATA_SHARDS</span><span class="value"
|
|
>Data shards for erasure coding (default: 4)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_ERASURE_PARITY_SHARDS</span><span class="value"
|
|
>Parity shards for erasure coding (default: 2)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_ERASURE_CHUNK_SIZE</span><span class="value"
|
|
>Chunk size in bytes (default: 4194304)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_HEARTBEAT_INTERVAL_MS</span><span class="value"
|
|
>Heartbeat interval in ms (default: 5000)</span>
|
|
</div>
|
|
<div class="row">
|
|
<span class="label">OBJST_HEARTBEAT_TIMEOUT_MS</span><span class="value"
|
|
>Heartbeat timeout in ms (default: 30000)</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private formatBytes(bytes: number): string {
|
|
if (bytes === 0) return '0 B';
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
}
|
|
}
|