bgrun 3.8.0 → 3.9.0
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/dashboard/app/globals.css +113 -0
- package/dashboard/app/page.client.tsx +107 -29
- package/dashboard/app/page.tsx +0 -1
- package/package.json +1 -1
|
@@ -989,6 +989,119 @@ tr.animate-in:nth-child(10) {
|
|
|
989
989
|
border-color: rgba(20, 184, 166, 0.3);
|
|
990
990
|
}
|
|
991
991
|
|
|
992
|
+
/* ─── Context Menu ─── */
|
|
993
|
+
.context-menu {
|
|
994
|
+
position: fixed;
|
|
995
|
+
z-index: 10000;
|
|
996
|
+
min-width: 180px;
|
|
997
|
+
background: rgba(22, 27, 34, 0.95);
|
|
998
|
+
backdrop-filter: blur(16px);
|
|
999
|
+
border: 1px solid var(--border);
|
|
1000
|
+
border-radius: 10px;
|
|
1001
|
+
padding: 4px;
|
|
1002
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.04);
|
|
1003
|
+
animation: contextIn 0.15s ease-out;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.context-menu.closing {
|
|
1007
|
+
animation: contextOut 0.12s ease-in forwards;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
@keyframes contextIn {
|
|
1011
|
+
from {
|
|
1012
|
+
opacity: 0;
|
|
1013
|
+
transform: scale(0.95) translateY(-4px);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
to {
|
|
1017
|
+
opacity: 1;
|
|
1018
|
+
transform: scale(1) translateY(0);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
@keyframes contextOut {
|
|
1023
|
+
from {
|
|
1024
|
+
opacity: 1;
|
|
1025
|
+
transform: scale(1) translateY(0);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
to {
|
|
1029
|
+
opacity: 0;
|
|
1030
|
+
transform: scale(0.95) translateY(-4px);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.context-item {
|
|
1035
|
+
display: flex;
|
|
1036
|
+
align-items: center;
|
|
1037
|
+
gap: 10px;
|
|
1038
|
+
width: 100%;
|
|
1039
|
+
padding: 8px 12px;
|
|
1040
|
+
background: none;
|
|
1041
|
+
border: none;
|
|
1042
|
+
border-radius: 6px;
|
|
1043
|
+
color: var(--text-secondary);
|
|
1044
|
+
font-size: 0.8125rem;
|
|
1045
|
+
cursor: pointer;
|
|
1046
|
+
transition: all 0.12s ease;
|
|
1047
|
+
text-align: left;
|
|
1048
|
+
font-family: inherit;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.context-item svg {
|
|
1052
|
+
width: 15px;
|
|
1053
|
+
height: 15px;
|
|
1054
|
+
flex-shrink: 0;
|
|
1055
|
+
opacity: 0.7;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.context-item:hover {
|
|
1059
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1060
|
+
color: var(--text-primary);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.context-item:hover svg {
|
|
1064
|
+
opacity: 1;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.context-item.danger:hover {
|
|
1068
|
+
background: var(--danger-bg);
|
|
1069
|
+
color: var(--danger);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.context-item.danger:hover svg {
|
|
1073
|
+
color: var(--danger);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.context-item.success:hover {
|
|
1077
|
+
background: var(--success-bg);
|
|
1078
|
+
color: var(--success);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.context-item.success:hover svg {
|
|
1082
|
+
color: var(--success);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.context-item.deploy:hover {
|
|
1086
|
+
background: rgba(20, 184, 166, 0.1);
|
|
1087
|
+
color: #14b8a6;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.context-item.deploy:hover svg {
|
|
1091
|
+
color: #14b8a6;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.context-divider {
|
|
1095
|
+
height: 1px;
|
|
1096
|
+
background: var(--border);
|
|
1097
|
+
margin: 4px 8px;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* Hint that rows are right-clickable */
|
|
1101
|
+
#processes-table tr[data-process-name] {
|
|
1102
|
+
cursor: default;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
992
1105
|
/* ─── Empty State ─── */
|
|
993
1106
|
.empty-state {
|
|
994
1107
|
text-align: center;
|
|
@@ -176,28 +176,6 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
|
176
176
|
</td>
|
|
177
177
|
<td className="command" title={p.command}>{p.command}</td>
|
|
178
178
|
<td className="runtime">{formatRuntime(p.runtime)}</td>
|
|
179
|
-
<td className="actions">
|
|
180
|
-
<button className="action-btn info" data-action="logs" data-name={p.name} title="View Logs">
|
|
181
|
-
<LogsIcon />
|
|
182
|
-
</button>
|
|
183
|
-
{p.running
|
|
184
|
-
? <button className="action-btn danger" data-action="stop" data-name={p.name} title="Stop">
|
|
185
|
-
<StopIcon />
|
|
186
|
-
</button>
|
|
187
|
-
: <button className="action-btn success" data-action="restart" data-name={p.name} title="Start">
|
|
188
|
-
<PlayIcon />
|
|
189
|
-
</button>
|
|
190
|
-
}
|
|
191
|
-
<button className="action-btn warning" data-action="restart" data-name={p.name} title="Restart">
|
|
192
|
-
<RestartIcon />
|
|
193
|
-
</button>
|
|
194
|
-
<button className="action-btn deploy" data-action="deploy" data-name={p.name} title="Deploy (git pull + restart)">
|
|
195
|
-
<DeployIcon />
|
|
196
|
-
</button>
|
|
197
|
-
<button className="action-btn danger" data-action="delete" data-name={p.name} title="Delete">
|
|
198
|
-
<TrashIcon />
|
|
199
|
-
</button>
|
|
200
|
-
</td>
|
|
201
179
|
</tr>
|
|
202
180
|
);
|
|
203
181
|
}
|
|
@@ -638,20 +616,99 @@ export default function mount(): () => void {
|
|
|
638
616
|
|
|
639
617
|
const tbody = $('processes-table');
|
|
640
618
|
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
619
|
+
// ─── Context Menu ───
|
|
620
|
+
let contextMenuEl: HTMLElement | null = null;
|
|
621
|
+
|
|
622
|
+
function closeContextMenu() {
|
|
623
|
+
if (contextMenuEl) {
|
|
624
|
+
contextMenuEl.classList.add('closing');
|
|
625
|
+
setTimeout(() => { contextMenuEl?.remove(); contextMenuEl = null; }, 150);
|
|
647
626
|
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function showContextMenu(name: string, x: number, y: number) {
|
|
630
|
+
closeContextMenu();
|
|
631
|
+
const proc = allProcesses.find(p => p.name === name);
|
|
632
|
+
if (!proc) return;
|
|
633
|
+
|
|
634
|
+
const menu = (
|
|
635
|
+
<div className="context-menu" style={{ left: `${x}px`, top: `${y}px` }}>
|
|
636
|
+
<button className="context-item" data-action="logs" data-name={name}>
|
|
637
|
+
<LogsIcon /> View Logs
|
|
638
|
+
</button>
|
|
639
|
+
<div className="context-divider"></div>
|
|
640
|
+
{proc.running
|
|
641
|
+
? <button className="context-item danger" data-action="stop" data-name={name}>
|
|
642
|
+
<StopIcon /> Stop
|
|
643
|
+
</button>
|
|
644
|
+
: <button className="context-item success" data-action="restart" data-name={name}>
|
|
645
|
+
<PlayIcon /> Start
|
|
646
|
+
</button>
|
|
647
|
+
}
|
|
648
|
+
<button className="context-item" data-action="restart" data-name={name}>
|
|
649
|
+
<RestartIcon /> Restart
|
|
650
|
+
</button>
|
|
651
|
+
<button className="context-item deploy" data-action="deploy" data-name={name}>
|
|
652
|
+
<DeployIcon /> Deploy
|
|
653
|
+
</button>
|
|
654
|
+
<div className="context-divider"></div>
|
|
655
|
+
<button className="context-item danger" data-action="delete" data-name={name}>
|
|
656
|
+
<TrashIcon /> Delete
|
|
657
|
+
</button>
|
|
658
|
+
</div>
|
|
659
|
+
) as unknown as HTMLElement;
|
|
660
|
+
|
|
661
|
+
// Handle clicks inside the menu
|
|
662
|
+
menu.addEventListener('click', (e: Event) => {
|
|
663
|
+
const item = (e.target as Element).closest('[data-action]');
|
|
664
|
+
if (item) {
|
|
665
|
+
handleAction(e);
|
|
666
|
+
closeContextMenu();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
document.body.appendChild(menu);
|
|
671
|
+
contextMenuEl = menu;
|
|
672
|
+
|
|
673
|
+
// Adjust position if menu goes off-screen
|
|
674
|
+
const rect = menu.getBoundingClientRect();
|
|
675
|
+
if (rect.right > window.innerWidth) menu.style.left = `${x - rect.width}px`;
|
|
676
|
+
if (rect.bottom > window.innerHeight) menu.style.top = `${y - rect.height}px`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Right-click on table rows → context menu
|
|
680
|
+
tbody?.addEventListener('contextmenu', (e: Event) => {
|
|
681
|
+
const me = e as MouseEvent;
|
|
682
|
+
const row = (me.target as Element).closest('tr[data-process-name]') as HTMLElement;
|
|
683
|
+
if (row && row.dataset.processName) {
|
|
684
|
+
me.preventDefault();
|
|
685
|
+
showContextMenu(row.dataset.processName, me.clientX, me.clientY);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Close context menu on click outside or Escape
|
|
690
|
+
document.addEventListener('click', (e: Event) => {
|
|
691
|
+
if (contextMenuEl && !contextMenuEl.contains(e.target as Node)) {
|
|
692
|
+
closeContextMenu();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
document.addEventListener('contextmenu', (e: Event) => {
|
|
696
|
+
// Allow right-click to close existing menu when clicking elsewhere
|
|
697
|
+
if (contextMenuEl && !contextMenuEl.contains(e.target as Node)) {
|
|
698
|
+
const row = (e.target as Element).closest('tr[data-process-name]');
|
|
699
|
+
if (!row) closeContextMenu();
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Row click → open drawer (left click)
|
|
704
|
+
tbody?.addEventListener('click', (e: Event) => {
|
|
648
705
|
const row = (e.target as Element).closest('tr[data-process-name]') as HTMLElement;
|
|
649
706
|
if (row && row.dataset.processName) {
|
|
650
707
|
openDrawer(row.dataset.processName);
|
|
651
708
|
}
|
|
652
709
|
});
|
|
653
710
|
|
|
654
|
-
// Mobile cards click →
|
|
711
|
+
// Mobile cards click → keep inline buttons
|
|
655
712
|
const mobileCards = $('mobile-cards');
|
|
656
713
|
mobileCards?.addEventListener('click', (e: Event) => {
|
|
657
714
|
const btn = (e.target as Element).closest('[data-action]');
|
|
@@ -665,6 +722,22 @@ export default function mount(): () => void {
|
|
|
665
722
|
}
|
|
666
723
|
});
|
|
667
724
|
|
|
725
|
+
// Mobile cards → context menu on long-press
|
|
726
|
+
let longPressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
727
|
+
mobileCards?.addEventListener('touchstart', (e: Event) => {
|
|
728
|
+
const te = e as TouchEvent;
|
|
729
|
+
const card = (te.target as Element).closest('.process-card[data-process-name]') as HTMLElement;
|
|
730
|
+
if (card && card.dataset.processName) {
|
|
731
|
+
const name = card.dataset.processName;
|
|
732
|
+
const touch = te.touches[0];
|
|
733
|
+
longPressTimer = setTimeout(() => {
|
|
734
|
+
showContextMenu(name, touch.clientX, touch.clientY);
|
|
735
|
+
}, 500);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
mobileCards?.addEventListener('touchend', () => { if (longPressTimer) clearTimeout(longPressTimer); });
|
|
739
|
+
mobileCards?.addEventListener('touchmove', () => { if (longPressTimer) clearTimeout(longPressTimer); });
|
|
740
|
+
|
|
668
741
|
// ─── Detail Drawer ───
|
|
669
742
|
|
|
670
743
|
const drawer = $('detail-drawer');
|
|
@@ -1196,6 +1269,10 @@ export default function mount(): () => void {
|
|
|
1196
1269
|
return;
|
|
1197
1270
|
}
|
|
1198
1271
|
if (e.key === 'Escape') {
|
|
1272
|
+
if (contextMenuEl) {
|
|
1273
|
+
closeContextMenu();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1199
1276
|
if (drawer?.classList.contains('open')) {
|
|
1200
1277
|
closeDrawer();
|
|
1201
1278
|
} else {
|
|
@@ -1255,6 +1332,7 @@ export default function mount(): () => void {
|
|
|
1255
1332
|
$('modal-create-btn')?.removeEventListener('click', createProcess);
|
|
1256
1333
|
$('refresh-btn')?.removeEventListener('click', loadProcesses);
|
|
1257
1334
|
document.removeEventListener('keydown', handleKeydown);
|
|
1335
|
+
closeContextMenu();
|
|
1258
1336
|
if (eventSource) eventSource.close();
|
|
1259
1337
|
if (logRefreshTimer) clearInterval(logRefreshTimer);
|
|
1260
1338
|
if (sseThrottleTimer) clearTimeout(sseThrottleTimer);
|
package/dashboard/app/page.tsx
CHANGED