@smartnet360/svelte-components 0.0.114 → 0.0.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/CellTable/CellTable.svelte +14 -0
- package/dist/core/CellTable/CellTable.svelte.d.ts +3 -1
- package/dist/core/CellTable/CellTableDemo.svelte +46 -1
- package/dist/core/CellTable/CellTablePanel.svelte +60 -1
- package/dist/core/CellTable/CellTablePanel.svelte.d.ts +5 -0
- package/dist/core/CellTable/CellTableToolbar.svelte +2 -1
- package/dist/core/CellTable/column-config.d.ts +17 -0
- package/dist/core/CellTable/column-config.js +132 -15
- package/dist/core/CellTable/index.d.ts +1 -1
- package/dist/core/CellTable/types.d.ts +9 -1
- package/dist/core/CoverageMap/data/SiteStore.js +2 -2
- package/dist/map-v2/demo/demo-cells.js +4 -4
- package/dist/map-v2/features/cells/types.d.ts +4 -4
- package/dist/shared/demo/cell-generator.js +24 -4
- package/dist/shared/demo/cell-types.d.ts +8 -4
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
RowSelectionEvent,
|
|
11
11
|
RowClickEvent,
|
|
12
12
|
RowDblClickEvent,
|
|
13
|
+
RowContextMenuEvent,
|
|
13
14
|
DataChangeEvent
|
|
14
15
|
} from './types';
|
|
15
16
|
import {
|
|
@@ -34,6 +35,8 @@
|
|
|
34
35
|
onrowclick?: (event: RowClickEvent) => void;
|
|
35
36
|
/** Row double-click event */
|
|
36
37
|
onrowdblclick?: (event: RowDblClickEvent) => void;
|
|
38
|
+
/** Row context menu (right-click) event */
|
|
39
|
+
onrowcontextmenu?: (event: RowContextMenuEvent) => void;
|
|
37
40
|
/** Data change event (filter, sort, etc.) */
|
|
38
41
|
ondatachange?: (event: DataChangeEvent) => void;
|
|
39
42
|
}
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
onselectionchange,
|
|
59
62
|
onrowclick,
|
|
60
63
|
onrowdblclick,
|
|
64
|
+
onrowcontextmenu,
|
|
61
65
|
ondatachange
|
|
62
66
|
}: Props = $props();
|
|
63
67
|
|
|
@@ -152,6 +156,16 @@
|
|
|
152
156
|
}
|
|
153
157
|
});
|
|
154
158
|
|
|
159
|
+
table.on('rowContext', (e, row) => {
|
|
160
|
+
if (onrowcontextmenu) {
|
|
161
|
+
(e as MouseEvent).preventDefault();
|
|
162
|
+
onrowcontextmenu({
|
|
163
|
+
row: (row as RowComponent).getData() as CellData,
|
|
164
|
+
event: e as MouseEvent
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
155
169
|
table.on('rowSelectionChanged', (data, rows) => {
|
|
156
170
|
if (onselectionchange) {
|
|
157
171
|
const cellData = data as CellData[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
|
2
2
|
import 'tabulator-tables/dist/css/tabulator_bootstrap5.min.css';
|
|
3
|
-
import type { CellTableProps, CellData, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent } from './types';
|
|
3
|
+
import type { CellTableProps, CellData, RowSelectionEvent, RowClickEvent, RowDblClickEvent, RowContextMenuEvent, DataChangeEvent } from './types';
|
|
4
4
|
interface Props extends CellTableProps {
|
|
5
5
|
/** Row selection change event */
|
|
6
6
|
onselectionchange?: (event: RowSelectionEvent) => void;
|
|
@@ -8,6 +8,8 @@ interface Props extends CellTableProps {
|
|
|
8
8
|
onrowclick?: (event: RowClickEvent) => void;
|
|
9
9
|
/** Row double-click event */
|
|
10
10
|
onrowdblclick?: (event: RowDblClickEvent) => void;
|
|
11
|
+
/** Row context menu (right-click) event */
|
|
12
|
+
onrowcontextmenu?: (event: RowContextMenuEvent) => void;
|
|
11
13
|
/** Data change event (filter, sort, etc.) */
|
|
12
14
|
ondatachange?: (event: DataChangeEvent) => void;
|
|
13
15
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CellTablePanel from './CellTablePanel.svelte';
|
|
3
|
-
import type { CellTableGroupField, ColumnPreset, RowSelectionEvent } from './types';
|
|
3
|
+
import type { CellTableGroupField, ColumnPreset, RowSelectionEvent, CellData } from './types';
|
|
4
4
|
import { generateCellsFromPreset, getGeneratorInfo, type GeneratorPreset } from '../../shared/demo';
|
|
5
5
|
|
|
6
6
|
let datasetSize: GeneratorPreset = $state('small');
|
|
@@ -42,6 +42,29 @@
|
|
|
42
42
|
console.log('Selection changed:', event.ids);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Context menu action handlers
|
|
46
|
+
function handleViewCell(cell: CellData) {
|
|
47
|
+
console.log('View cell:', cell.id, cell.cellName);
|
|
48
|
+
alert(`Viewing cell: ${cell.cellName}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleEditCell(cell: CellData) {
|
|
52
|
+
console.log('Edit cell:', cell.id, cell.cellName);
|
|
53
|
+
alert(`Edit cell: ${cell.cellName}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleDeleteCell(cell: CellData) {
|
|
57
|
+
console.log('Delete cell:', cell.id, cell.cellName);
|
|
58
|
+
if (confirm(`Delete cell ${cell.cellName}?`)) {
|
|
59
|
+
demoCells = demoCells.filter(c => c.id !== cell.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleShowOnMap(cell: CellData) {
|
|
64
|
+
console.log('Show on map:', cell.latitude, cell.longitude);
|
|
65
|
+
alert(`Show on map: ${cell.cellName}\nLat: ${cell.latitude}\nLon: ${cell.longitude}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
function regenerateData() {
|
|
46
69
|
demoCells = generateCellsFromPreset(datasetSize);
|
|
47
70
|
}
|
|
@@ -242,6 +265,28 @@
|
|
|
242
265
|
</div>
|
|
243
266
|
</div>
|
|
244
267
|
{/snippet}
|
|
268
|
+
|
|
269
|
+
{#snippet contextMenu({ row, closeMenu })}
|
|
270
|
+
<div class="dropdown-menu show shadow-lg" style="min-width: 180px;">
|
|
271
|
+
<h6 class="dropdown-header text-truncate" style="max-width: 200px;">
|
|
272
|
+
<i class="bi bi-broadcast me-1"></i>{row.cellName}
|
|
273
|
+
</h6>
|
|
274
|
+
<div class="dropdown-divider"></div>
|
|
275
|
+
<button class="dropdown-item" onclick={() => { handleViewCell(row); closeMenu(); }}>
|
|
276
|
+
<i class="bi bi-eye me-2 text-primary"></i>View Details
|
|
277
|
+
</button>
|
|
278
|
+
<button class="dropdown-item" onclick={() => { handleEditCell(row); closeMenu(); }}>
|
|
279
|
+
<i class="bi bi-pencil me-2 text-warning"></i>Edit Cell
|
|
280
|
+
</button>
|
|
281
|
+
<button class="dropdown-item" onclick={() => { handleShowOnMap(row); closeMenu(); }}>
|
|
282
|
+
<i class="bi bi-geo-alt me-2 text-info"></i>Show on Map
|
|
283
|
+
</button>
|
|
284
|
+
<div class="dropdown-divider"></div>
|
|
285
|
+
<button class="dropdown-item text-danger" onclick={() => { handleDeleteCell(row); closeMenu(); }}>
|
|
286
|
+
<i class="bi bi-trash me-2"></i>Delete Cell
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
{/snippet}
|
|
245
290
|
</CellTablePanel>
|
|
246
291
|
</div>
|
|
247
292
|
</div>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
RowSelectionEvent,
|
|
11
11
|
RowClickEvent,
|
|
12
12
|
RowDblClickEvent,
|
|
13
|
+
RowContextMenuEvent,
|
|
13
14
|
DataChangeEvent,
|
|
14
15
|
TechColorMap,
|
|
15
16
|
StatusColorMap
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
footer?: Snippet<[{ selectedRows: CellData[]; selectedCount: number }]>;
|
|
69
70
|
/** Custom details sidebar content */
|
|
70
71
|
detailsContent?: Snippet<[{ cell: CellData | null; closeSidebar: () => void }]>;
|
|
72
|
+
/** Custom context menu content (right-click on row) */
|
|
73
|
+
contextMenu?: Snippet<[{ row: CellData; closeMenu: () => void }]>;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
let {
|
|
@@ -96,9 +99,15 @@
|
|
|
96
99
|
headerSearch,
|
|
97
100
|
headerActions,
|
|
98
101
|
footer,
|
|
99
|
-
detailsContent
|
|
102
|
+
detailsContent,
|
|
103
|
+
contextMenu
|
|
100
104
|
}: Props = $props();
|
|
101
105
|
|
|
106
|
+
// Context menu state
|
|
107
|
+
let contextMenuVisible = $state(false);
|
|
108
|
+
let contextMenuRow: CellData | null = $state(null);
|
|
109
|
+
let contextMenuPosition = $state({ x: 0, y: 0 });
|
|
110
|
+
|
|
102
111
|
// Storage keys
|
|
103
112
|
const STORAGE_KEY_GROUP = `${storageKey}-groupBy`;
|
|
104
113
|
const STORAGE_KEY_COLUMNS = `${storageKey}-visibleColumns`;
|
|
@@ -239,6 +248,26 @@
|
|
|
239
248
|
updateScrollSpyGroups();
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
function handleRowContextMenu(event: RowContextMenuEvent) {
|
|
252
|
+
if (contextMenu) {
|
|
253
|
+
contextMenuRow = event.row;
|
|
254
|
+
contextMenuPosition = { x: event.event.clientX, y: event.event.clientY };
|
|
255
|
+
contextMenuVisible = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function closeContextMenu() {
|
|
260
|
+
contextMenuVisible = false;
|
|
261
|
+
contextMenuRow = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function handleClickOutside(event: MouseEvent) {
|
|
265
|
+
const target = event.target as HTMLElement;
|
|
266
|
+
if (!target.closest('.context-menu-container')) {
|
|
267
|
+
closeContextMenu();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
function updateScrollSpyGroups() {
|
|
243
272
|
if (scrollSpyEnabled && groupBy !== 'none') {
|
|
244
273
|
// Small delay to ensure table has updated
|
|
@@ -477,6 +506,7 @@
|
|
|
477
506
|
onselectionchange={handleSelectionChange}
|
|
478
507
|
ondatachange={handleDataChange}
|
|
479
508
|
onrowclick={handleRowClick}
|
|
509
|
+
onrowcontextmenu={handleRowContextMenu}
|
|
480
510
|
{onrowdblclick}
|
|
481
511
|
/>
|
|
482
512
|
</div>
|
|
@@ -617,8 +647,21 @@
|
|
|
617
647
|
{@render footer({ selectedRows, selectedCount })}
|
|
618
648
|
</div>
|
|
619
649
|
{/if}
|
|
650
|
+
|
|
651
|
+
<!-- Context Menu (portal-like, fixed position) -->
|
|
652
|
+
{#if contextMenuVisible && contextMenuRow && contextMenu}
|
|
653
|
+
<div
|
|
654
|
+
class="context-menu-container"
|
|
655
|
+
style="position: fixed; left: {contextMenuPosition.x}px; top: {contextMenuPosition.y}px; z-index: 1050;"
|
|
656
|
+
>
|
|
657
|
+
{@render contextMenu({ row: contextMenuRow, closeMenu: closeContextMenu })}
|
|
658
|
+
</div>
|
|
659
|
+
{/if}
|
|
620
660
|
</div>
|
|
621
661
|
|
|
662
|
+
<!-- Click outside handler -->
|
|
663
|
+
<svelte:window onclick={contextMenuVisible ? handleClickOutside : undefined} />
|
|
664
|
+
|
|
622
665
|
<style>
|
|
623
666
|
.cell-table-panel {
|
|
624
667
|
background-color: var(--bs-body-bg, #fff);
|
|
@@ -694,4 +737,20 @@
|
|
|
694
737
|
.scrollspy-badge:active {
|
|
695
738
|
transform: translateY(0);
|
|
696
739
|
}
|
|
740
|
+
|
|
741
|
+
/* Context menu styling */
|
|
742
|
+
.context-menu-container {
|
|
743
|
+
animation: contextMenuFadeIn 0.1s ease-out;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@keyframes contextMenuFadeIn {
|
|
747
|
+
from {
|
|
748
|
+
opacity: 0;
|
|
749
|
+
transform: scale(0.95);
|
|
750
|
+
}
|
|
751
|
+
to {
|
|
752
|
+
opacity: 1;
|
|
753
|
+
transform: scale(1);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
697
756
|
</style>
|
|
@@ -61,6 +61,11 @@ interface Props {
|
|
|
61
61
|
cell: CellData | null;
|
|
62
62
|
closeSidebar: () => void;
|
|
63
63
|
}]>;
|
|
64
|
+
/** Custom context menu content (right-click on row) */
|
|
65
|
+
contextMenu?: Snippet<[{
|
|
66
|
+
row: CellData;
|
|
67
|
+
closeMenu: () => void;
|
|
68
|
+
}]>;
|
|
64
69
|
}
|
|
65
70
|
declare const CellTablePanel: import("svelte").Component<Props, {
|
|
66
71
|
getSelectedRows: () => CellData[];
|
|
@@ -100,7 +100,8 @@
|
|
|
100
100
|
{ value: 'physical', label: 'Physical' },
|
|
101
101
|
{ value: 'network', label: 'Network' },
|
|
102
102
|
{ value: 'planning', label: 'Planning' },
|
|
103
|
-
{ value: 'compare', label: 'Compare (Atoll/
|
|
103
|
+
{ value: 'compare', label: 'Compare (Atoll/NW)' },
|
|
104
|
+
{ value: 'kpi', label: 'KPI Trends' }
|
|
104
105
|
];
|
|
105
106
|
|
|
106
107
|
function handleGroupChange(e: Event) {
|
|
@@ -61,6 +61,23 @@ export declare function heightFormatter(cell: CellComponent): string;
|
|
|
61
61
|
* - Gray: one or both values missing
|
|
62
62
|
*/
|
|
63
63
|
export declare function createCompareFormatter(atollField: string, nwtField: string): (cell: CellComponent) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a sparkline SVG formatter for KPI time-series data
|
|
66
|
+
* Renders a mini line chart showing trend over time
|
|
67
|
+
*
|
|
68
|
+
* @param options Configuration for the sparkline
|
|
69
|
+
*/
|
|
70
|
+
export interface SparklineOptions {
|
|
71
|
+
width?: number;
|
|
72
|
+
height?: number;
|
|
73
|
+
lineColor?: string;
|
|
74
|
+
fillColor?: string;
|
|
75
|
+
showDots?: boolean;
|
|
76
|
+
showLastValue?: boolean;
|
|
77
|
+
unit?: string;
|
|
78
|
+
decimals?: number;
|
|
79
|
+
}
|
|
80
|
+
export declare function createSparklineFormatter(options?: SparklineOptions): (cell: CellComponent) => string;
|
|
64
81
|
/**
|
|
65
82
|
* Custom sorter for fband - extracts numeric portion and sorts numerically
|
|
66
83
|
* Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
|
|
@@ -55,11 +55,12 @@ export const FBAND_COLORS = {
|
|
|
55
55
|
export const COLUMN_GROUPS = {
|
|
56
56
|
core: ['siteId', 'txId', 'cellName', 'tech', 'fband', 'status'],
|
|
57
57
|
physical: ['antenna', 'azimuth', 'height', 'electricalTilt', 'beamwidth'],
|
|
58
|
-
network: ['dlEarfn', 'bcch', 'pci1', 'cellId3', '
|
|
58
|
+
network: ['dlEarfn', 'bcch', 'pci1', 'cellId3', 'nwET', 'nwPW', 'nwRS', 'nwBW'],
|
|
59
59
|
planning: ['planner', 'comment', 'onAirDate', 'type'],
|
|
60
60
|
atoll: ['atollET', 'atollPW', 'atollRS', 'atollBW'],
|
|
61
61
|
position: ['latitude', 'longitude', 'siteLatitude', 'siteLongitude', 'dx', 'dy'],
|
|
62
62
|
compare: ['compareET', 'comparePW', 'compareRS', 'compareBW'],
|
|
63
|
+
kpi: ['kpiTraffic', 'kpiThroughput', 'kpiAvailability', 'kpiSuccessRate'],
|
|
63
64
|
};
|
|
64
65
|
/**
|
|
65
66
|
* Create a technology badge formatter
|
|
@@ -166,6 +167,55 @@ export function createCompareFormatter(atollField, nwtField) {
|
|
|
166
167
|
return `<span class="badge" style="background-color: ${bgColor}; color: ${textColor}; font-size: 0.75rem; font-weight: normal;">${atollStr} | ${nwtStr}</span>`;
|
|
167
168
|
};
|
|
168
169
|
}
|
|
170
|
+
export function createSparklineFormatter(options = {}) {
|
|
171
|
+
const { width = 80, height = 24, lineColor = '#0d6efd', fillColor = 'rgba(13,110,253,0.2)', showDots = false, showLastValue = true, unit = '', decimals = 1 } = options;
|
|
172
|
+
return (cell) => {
|
|
173
|
+
const data = cell.getValue();
|
|
174
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
175
|
+
return '<span class="text-muted">—</span>';
|
|
176
|
+
}
|
|
177
|
+
const values = data.filter(v => typeof v === 'number' && !isNaN(v));
|
|
178
|
+
if (values.length === 0) {
|
|
179
|
+
return '<span class="text-muted">—</span>';
|
|
180
|
+
}
|
|
181
|
+
const min = Math.min(...values);
|
|
182
|
+
const max = Math.max(...values);
|
|
183
|
+
const range = max - min || 1;
|
|
184
|
+
const padding = 2;
|
|
185
|
+
const chartWidth = showLastValue ? width - 35 : width - 4;
|
|
186
|
+
const chartHeight = height - 4;
|
|
187
|
+
// Generate SVG path
|
|
188
|
+
const points = values.map((v, i) => {
|
|
189
|
+
const x = padding + (i / (values.length - 1 || 1)) * (chartWidth - padding * 2);
|
|
190
|
+
const y = padding + (1 - (v - min) / range) * (chartHeight - padding * 2);
|
|
191
|
+
return { x, y, v };
|
|
192
|
+
});
|
|
193
|
+
// Line path
|
|
194
|
+
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(' ');
|
|
195
|
+
// Fill path (closed polygon)
|
|
196
|
+
const fillPath = `${linePath} L ${points[points.length - 1].x.toFixed(1)} ${chartHeight - padding} L ${padding} ${chartHeight - padding} Z`;
|
|
197
|
+
// Dots
|
|
198
|
+
const dots = showDots
|
|
199
|
+
? points.map(p => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="1.5" fill="${lineColor}"/>`).join('')
|
|
200
|
+
: '';
|
|
201
|
+
// Last value text
|
|
202
|
+
const lastValue = values[values.length - 1];
|
|
203
|
+
const valueText = showLastValue
|
|
204
|
+
? `<text x="${width - 2}" y="${height / 2 + 4}" text-anchor="end" font-size="10" fill="#333">${lastValue.toFixed(decimals)}${unit}</text>`
|
|
205
|
+
: '';
|
|
206
|
+
// Trend indicator (comparing last vs first)
|
|
207
|
+
const trend = values.length > 1 ? values[values.length - 1] - values[0] : 0;
|
|
208
|
+
const trendColor = trend >= 0 ? '#198754' : '#dc3545';
|
|
209
|
+
return `
|
|
210
|
+
<svg width="${width}" height="${height}" style="vertical-align: middle;">
|
|
211
|
+
<path d="${fillPath}" fill="${fillColor}" />
|
|
212
|
+
<path d="${linePath}" fill="none" stroke="${lineColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
213
|
+
${dots}
|
|
214
|
+
${valueText}
|
|
215
|
+
</svg>
|
|
216
|
+
`.trim().replace(/\s+/g, ' ');
|
|
217
|
+
};
|
|
218
|
+
}
|
|
169
219
|
/**
|
|
170
220
|
* Custom sorter for fband - extracts numeric portion and sorts numerically
|
|
171
221
|
* Examples: LTE700 → 700, GSM900 → 900, LTE1800 → 1800, 5G-3500 → 3500
|
|
@@ -363,28 +413,28 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
363
413
|
},
|
|
364
414
|
{
|
|
365
415
|
title: 'NWT P1',
|
|
366
|
-
field: '
|
|
416
|
+
field: 'nwP1',
|
|
367
417
|
width: 80,
|
|
368
418
|
hozAlign: 'right',
|
|
369
419
|
...headerFilterParams,
|
|
370
420
|
},
|
|
371
421
|
{
|
|
372
422
|
title: 'NWT P2',
|
|
373
|
-
field: '
|
|
423
|
+
field: 'nwP2',
|
|
374
424
|
width: 80,
|
|
375
425
|
hozAlign: 'right',
|
|
376
426
|
...headerFilterParams,
|
|
377
427
|
},
|
|
378
428
|
{
|
|
379
|
-
title: '
|
|
380
|
-
field: '
|
|
429
|
+
title: 'NW RS',
|
|
430
|
+
field: 'nwRS',
|
|
381
431
|
width: 80,
|
|
382
432
|
hozAlign: 'right',
|
|
383
433
|
...headerFilterParams,
|
|
384
434
|
},
|
|
385
435
|
{
|
|
386
|
-
title: '
|
|
387
|
-
field: '
|
|
436
|
+
title: 'NW BW',
|
|
437
|
+
field: 'nwBW',
|
|
388
438
|
width: 80,
|
|
389
439
|
hozAlign: 'right',
|
|
390
440
|
...headerFilterParams,
|
|
@@ -428,7 +478,7 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
428
478
|
field: 'compareET',
|
|
429
479
|
width: 110,
|
|
430
480
|
hozAlign: 'center',
|
|
431
|
-
formatter: createCompareFormatter('atollET', '
|
|
481
|
+
formatter: createCompareFormatter('atollET', 'nwET'),
|
|
432
482
|
headerTooltip: 'Atoll ET | Network ET',
|
|
433
483
|
},
|
|
434
484
|
{
|
|
@@ -436,7 +486,7 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
436
486
|
field: 'comparePW',
|
|
437
487
|
width: 110,
|
|
438
488
|
hozAlign: 'center',
|
|
439
|
-
formatter: createCompareFormatter('atollPW', '
|
|
489
|
+
formatter: createCompareFormatter('atollPW', 'nwPW'),
|
|
440
490
|
headerTooltip: 'Atoll PW | Network PW',
|
|
441
491
|
},
|
|
442
492
|
{
|
|
@@ -444,7 +494,7 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
444
494
|
field: 'compareRS',
|
|
445
495
|
width: 110,
|
|
446
496
|
hozAlign: 'center',
|
|
447
|
-
formatter: createCompareFormatter('atollRS', '
|
|
497
|
+
formatter: createCompareFormatter('atollRS', 'nwRS'),
|
|
448
498
|
headerTooltip: 'Atoll RS | Network RS',
|
|
449
499
|
},
|
|
450
500
|
{
|
|
@@ -452,9 +502,66 @@ export function getAllColumns(techColors = DEFAULT_TECH_COLORS, statusColors = D
|
|
|
452
502
|
field: 'compareBW',
|
|
453
503
|
width: 110,
|
|
454
504
|
hozAlign: 'center',
|
|
455
|
-
formatter: createCompareFormatter('atollBW', '
|
|
505
|
+
formatter: createCompareFormatter('atollBW', 'nwBW'),
|
|
456
506
|
headerTooltip: 'Atoll BW | Network BW',
|
|
457
507
|
},
|
|
508
|
+
// KPI Sparkline columns
|
|
509
|
+
{
|
|
510
|
+
title: 'Traffic',
|
|
511
|
+
field: 'kpiTraffic',
|
|
512
|
+
width: 120,
|
|
513
|
+
hozAlign: 'center',
|
|
514
|
+
formatter: createSparklineFormatter({
|
|
515
|
+
unit: '',
|
|
516
|
+
lineColor: '#0d6efd',
|
|
517
|
+
fillColor: 'rgba(13,110,253,0.15)',
|
|
518
|
+
decimals: 0
|
|
519
|
+
}),
|
|
520
|
+
headerTooltip: 'Traffic volume trend (GB)',
|
|
521
|
+
headerSort: false,
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
title: 'Throughput',
|
|
525
|
+
field: 'kpiThroughput',
|
|
526
|
+
width: 120,
|
|
527
|
+
hozAlign: 'center',
|
|
528
|
+
formatter: createSparklineFormatter({
|
|
529
|
+
unit: '',
|
|
530
|
+
lineColor: '#6f42c1',
|
|
531
|
+
fillColor: 'rgba(111,66,193,0.15)',
|
|
532
|
+
decimals: 1
|
|
533
|
+
}),
|
|
534
|
+
headerTooltip: 'Throughput trend (Mbps)',
|
|
535
|
+
headerSort: false,
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
title: 'Avail %',
|
|
539
|
+
field: 'kpiAvailability',
|
|
540
|
+
width: 120,
|
|
541
|
+
hozAlign: 'center',
|
|
542
|
+
formatter: createSparklineFormatter({
|
|
543
|
+
unit: '%',
|
|
544
|
+
lineColor: '#198754',
|
|
545
|
+
fillColor: 'rgba(25,135,84,0.15)',
|
|
546
|
+
decimals: 1
|
|
547
|
+
}),
|
|
548
|
+
headerTooltip: 'Availability trend (%)',
|
|
549
|
+
headerSort: false,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
title: 'Success %',
|
|
553
|
+
field: 'kpiSuccessRate',
|
|
554
|
+
width: 120,
|
|
555
|
+
hozAlign: 'center',
|
|
556
|
+
formatter: createSparklineFormatter({
|
|
557
|
+
unit: '%',
|
|
558
|
+
lineColor: '#fd7e14',
|
|
559
|
+
fillColor: 'rgba(253,126,20,0.15)',
|
|
560
|
+
decimals: 1
|
|
561
|
+
}),
|
|
562
|
+
headerTooltip: 'Success rate trend (%)',
|
|
563
|
+
headerSort: false,
|
|
564
|
+
},
|
|
458
565
|
// Position columns
|
|
459
566
|
{
|
|
460
567
|
title: 'Latitude',
|
|
@@ -549,6 +656,9 @@ export function getColumnsForPreset(preset, techColors = DEFAULT_TECH_COLORS, st
|
|
|
549
656
|
case 'compare':
|
|
550
657
|
visibleFields = [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.compare];
|
|
551
658
|
break;
|
|
659
|
+
case 'kpi':
|
|
660
|
+
visibleFields = [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.kpi];
|
|
661
|
+
break;
|
|
552
662
|
case 'default':
|
|
553
663
|
default:
|
|
554
664
|
visibleFields = [
|
|
@@ -607,10 +717,10 @@ export function getColumnMetadata() {
|
|
|
607
717
|
{ field: 'cellID', title: 'Cell ID', group: 'Network' },
|
|
608
718
|
{ field: 'cellId2G', title: 'Cell ID 2G', group: 'Network' },
|
|
609
719
|
{ field: 'ctrlid', title: 'Ctrl ID', group: 'Network' },
|
|
610
|
-
{ field: '
|
|
611
|
-
{ field: '
|
|
612
|
-
{ field: '
|
|
613
|
-
{ field: '
|
|
720
|
+
{ field: 'nwET', title: 'NW ET', group: 'Network' },
|
|
721
|
+
{ field: 'nwPW', title: 'NW PW', group: 'Network' },
|
|
722
|
+
{ field: 'nwRS', title: 'NW RS', group: 'Network' },
|
|
723
|
+
{ field: 'nwBW', title: 'NW BW', group: 'Network' },
|
|
614
724
|
// Atoll
|
|
615
725
|
{ field: 'atollET', title: 'Atoll ET', group: 'Atoll' },
|
|
616
726
|
{ field: 'atollPW', title: 'Atoll PW', group: 'Atoll' },
|
|
@@ -621,6 +731,11 @@ export function getColumnMetadata() {
|
|
|
621
731
|
{ field: 'comparePW', title: 'Δ PW', group: 'Compare' },
|
|
622
732
|
{ field: 'compareRS', title: 'Δ RS', group: 'Compare' },
|
|
623
733
|
{ field: 'compareBW', title: 'Δ BW', group: 'Compare' },
|
|
734
|
+
// KPI Trends
|
|
735
|
+
{ field: 'kpiTraffic', title: 'Traffic', group: 'KPI' },
|
|
736
|
+
{ field: 'kpiThroughput', title: 'Throughput', group: 'KPI' },
|
|
737
|
+
{ field: 'kpiAvailability', title: 'Availability', group: 'KPI' },
|
|
738
|
+
{ field: 'kpiSuccessRate', title: 'Success Rate', group: 'KPI' },
|
|
624
739
|
// Position
|
|
625
740
|
{ field: 'latitude', title: 'Latitude', group: 'Position' },
|
|
626
741
|
{ field: 'longitude', title: 'Longitude', group: 'Position' },
|
|
@@ -651,6 +766,8 @@ export function getPresetVisibleFields(preset) {
|
|
|
651
766
|
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.planning];
|
|
652
767
|
case 'compare':
|
|
653
768
|
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.compare];
|
|
769
|
+
case 'kpi':
|
|
770
|
+
return [...COLUMN_GROUPS.core, ...COLUMN_GROUPS.kpi];
|
|
654
771
|
case 'default':
|
|
655
772
|
default:
|
|
656
773
|
return ['siteId', 'txId', 'cellName', 'tech', 'fband', 'frq', 'status', 'azimuth', 'height', 'antenna'];
|
|
@@ -9,5 +9,5 @@ export { default as CellTablePanel } from './CellTablePanel.svelte';
|
|
|
9
9
|
export { default as CellTableDemo } from './CellTableDemo.svelte';
|
|
10
10
|
export { default as ColumnPicker } from './ColumnPicker.svelte';
|
|
11
11
|
export { generateCells, generateCellsFromPreset, getGeneratorInfo, GENERATOR_PRESETS, type CellGeneratorConfig, type GeneratorPreset } from '../../shared/demo';
|
|
12
|
-
export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
|
|
12
|
+
export type { CellData, CellTableGroupField, ColumnPreset, ColumnVisibility, TechColorMap, StatusColorMap, CellTableProps, RowSelectionEvent, RowClickEvent, RowDblClickEvent, RowContextMenuEvent, DataChangeEvent, CellTableColumn, ColumnGroups } from './types';
|
|
13
13
|
export { DEFAULT_TECH_COLORS, DEFAULT_STATUS_COLORS, FBAND_COLORS, COLUMN_GROUPS, getAllColumns, getColumnsForPreset, getGroupHeaderFormatter, getColumnMetadata, getPresetVisibleFields, fbandSorter, cellNameSectorSorter, cellDataSorter, createTechFormatter, createStatusFormatter, createFbandFormatter, numberFormatter, coordinateFormatter, azimuthFormatter, heightFormatter, type ColumnMeta } from './column-config';
|
|
@@ -16,7 +16,7 @@ export type CellTableGroupField = CellGroupingField | 'none';
|
|
|
16
16
|
/**
|
|
17
17
|
* Column preset configurations
|
|
18
18
|
*/
|
|
19
|
-
export type ColumnPreset = 'default' | 'compact' | 'full' | 'physical' | 'network' | 'planning' | 'compare';
|
|
19
|
+
export type ColumnPreset = 'default' | 'compact' | 'full' | 'physical' | 'network' | 'planning' | 'compare' | 'kpi';
|
|
20
20
|
/**
|
|
21
21
|
* Column visibility configuration
|
|
22
22
|
*/
|
|
@@ -93,6 +93,13 @@ export interface RowDblClickEvent {
|
|
|
93
93
|
row: CellData;
|
|
94
94
|
event: MouseEvent;
|
|
95
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Row context menu (right-click) event data
|
|
98
|
+
*/
|
|
99
|
+
export interface RowContextMenuEvent {
|
|
100
|
+
row: CellData;
|
|
101
|
+
event: MouseEvent;
|
|
102
|
+
}
|
|
96
103
|
/**
|
|
97
104
|
* Data change event data
|
|
98
105
|
*/
|
|
@@ -123,4 +130,5 @@ export interface ColumnGroups {
|
|
|
123
130
|
atoll: string[];
|
|
124
131
|
position: string[];
|
|
125
132
|
compare: string[];
|
|
133
|
+
kpi: string[];
|
|
126
134
|
}
|
|
@@ -116,8 +116,8 @@ export class SiteStore {
|
|
|
116
116
|
availableTilts: ['0']
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
// Determine TX power (priority: atollPW >
|
|
120
|
-
const txPower = cell.atollPW || cell.
|
|
119
|
+
// Determine TX power (priority: atollPW > nwPW > default)
|
|
120
|
+
const txPower = cell.atollPW || cell.nwPW || 43; // Default to 43 dBm (20W)
|
|
121
121
|
// Determine frequency from band
|
|
122
122
|
const frequency = antennaPattern.frequency || this.parseFrequency(cell.fband);
|
|
123
123
|
// Get mechanical tilt (might need to parse from string)
|
|
@@ -195,11 +195,11 @@ for (let attempt = 0; attempt < NUM_SITES * 3 && actualSiteIndex < NUM_SITES; at
|
|
|
195
195
|
atollBW: parseFloat(techBand.band) / 100, // Simplified bandwidth
|
|
196
196
|
// Network properties
|
|
197
197
|
cellId3: `${cellId}-3G`,
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
nwP1: 20,
|
|
199
|
+
nwP2: 40,
|
|
200
200
|
pci1: (cellCounter % 504), // Physical Cell ID for LTE
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
nwRS: 450.0,
|
|
202
|
+
nwBW: 10.0,
|
|
203
203
|
// Other
|
|
204
204
|
other: {
|
|
205
205
|
demoCell: true,
|
|
@@ -40,11 +40,11 @@ export interface Cell {
|
|
|
40
40
|
atollRS: number;
|
|
41
41
|
atollBW: number;
|
|
42
42
|
cellId3: string;
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
nwP1: number;
|
|
44
|
+
nwP2: number;
|
|
45
45
|
pci1: number;
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
nwRS: number;
|
|
47
|
+
nwBW: number;
|
|
48
48
|
other?: Record<string, any>;
|
|
49
49
|
customSubgroup: string;
|
|
50
50
|
}
|
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
* Generates realistic cell network data with configurable parameters.
|
|
5
5
|
* Supports density zones, multiple technologies, and various site configurations.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate mock KPI time-series data
|
|
9
|
+
* Creates an array of values with realistic variation
|
|
10
|
+
*/
|
|
11
|
+
function generateMockKpiData(days, min, max, random) {
|
|
12
|
+
const data = [];
|
|
13
|
+
let current = min + random() * (max - min);
|
|
14
|
+
for (let i = 0; i < days; i++) {
|
|
15
|
+
// Add some realistic variation (±10%)
|
|
16
|
+
const variation = (random() - 0.5) * 0.2 * (max - min);
|
|
17
|
+
current = Math.max(min, Math.min(max, current + variation));
|
|
18
|
+
data.push(Math.round(current * 10) / 10);
|
|
19
|
+
}
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
7
22
|
/**
|
|
8
23
|
* Preset configurations
|
|
9
24
|
*/
|
|
@@ -206,11 +221,16 @@ export function generateCells(config) {
|
|
|
206
221
|
atollRS: 500.0 + (techBand.band === '700' ? 200 : 0),
|
|
207
222
|
atollBW: parseFloat(techBand.band) / 100,
|
|
208
223
|
rru: `RRU-${siteId}-${sector.sectorNum}`,
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
nwET: 40.0,
|
|
225
|
+
nwPW: 20,
|
|
211
226
|
pci: cellCounter % 504,
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
nwRS: 450.0,
|
|
228
|
+
nwBW: 10.0,
|
|
229
|
+
// KPI time-series mock data (7 days)
|
|
230
|
+
kpiTraffic: generateMockKpiData(7, 50, 200, random),
|
|
231
|
+
kpiThroughput: generateMockKpiData(7, 20, 80, random),
|
|
232
|
+
kpiAvailability: generateMockKpiData(7, 95, 100, random),
|
|
233
|
+
kpiSuccessRate: generateMockKpiData(7, 90, 100, random),
|
|
214
234
|
other: {
|
|
215
235
|
city: ['Tehran', 'Shiraz', 'Isfahan', 'Mashhad', 'Tabriz'][siteNum % 5],
|
|
216
236
|
bcc: bandIndex % 8,
|
|
@@ -39,12 +39,16 @@ export interface Cell {
|
|
|
39
39
|
atollPW?: number;
|
|
40
40
|
atollRS?: number;
|
|
41
41
|
atollBW?: number;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
nwET?: number;
|
|
43
|
+
nwPW?: number;
|
|
44
|
+
nwRS?: number;
|
|
45
|
+
nwBW?: number;
|
|
46
46
|
pci?: number;
|
|
47
47
|
rru: string;
|
|
48
|
+
kpiTraffic?: number[];
|
|
49
|
+
kpiThroughput?: number[];
|
|
50
|
+
kpiAvailability?: number[];
|
|
51
|
+
kpiSuccessRate?: number[];
|
|
48
52
|
other?: Record<string, unknown>;
|
|
49
53
|
customSubgroup: string;
|
|
50
54
|
}
|