198 lines
7.3 KiB
JavaScript
198 lines
7.3 KiB
JavaScript
import Echo from 'laravel-echo';
|
|
import Pusher from 'pusher-js';
|
|
|
|
window.Pusher = Pusher;
|
|
|
|
// Reverb (Pusher protocol). Host/port/scheme are env-driven (derived from APP_DOMAIN later).
|
|
window.Echo = new Echo({
|
|
broadcaster: 'reverb',
|
|
key: import.meta.env.VITE_REVERB_APP_KEY,
|
|
wsHost: import.meta.env.VITE_REVERB_HOST,
|
|
wsPort: Number(import.meta.env.VITE_REVERB_PORT ?? 80),
|
|
wssPort: Number(import.meta.env.VITE_REVERB_PORT ?? 443),
|
|
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
|
enabledTransports: ['ws', 'wss'],
|
|
});
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
// Live dual-series chart (CPU + MEM). Seeds from server-rendered data, then
|
|
// appends each MetricsTicked broadcast and redraws (viewBox 0 0 300 100).
|
|
window.Alpine.data('dualChart', (cpuSeed = [], memSeed = [], max = 40, server = null) => ({
|
|
cpu: cpuSeed.slice(-max),
|
|
mem: memSeed.slice(-max),
|
|
max,
|
|
server,
|
|
connected: false,
|
|
|
|
init() {
|
|
const conn = window.Echo?.connector?.pusher?.connection;
|
|
this.connected = conn?.state === 'connected';
|
|
this._onState = (s) => { this.connected = s.current === 'connected'; };
|
|
conn?.bind('state_change', this._onState);
|
|
this._channel = window.Echo?.channel('metrics');
|
|
this._channel?.listen('.tick', (e) => {
|
|
// only react to the server this chart is showing
|
|
if (this.server && e.server && e.server !== this.server) return;
|
|
this.append(this.cpu, e.cpu);
|
|
this.append(this.mem, e.mem);
|
|
});
|
|
},
|
|
|
|
destroy() {
|
|
window.Echo?.leave('metrics');
|
|
window.Echo?.connector?.pusher?.connection?.unbind('state_change', this._onState);
|
|
},
|
|
|
|
append(arr, v) {
|
|
const n = Number(v);
|
|
if (! Number.isFinite(n)) return;
|
|
arr.push(n);
|
|
if (arr.length > this.max) arr.shift();
|
|
},
|
|
|
|
_coords(arr) {
|
|
const n = arr.length, w = 300, h = 100, pad = 6;
|
|
return arr
|
|
.map((v, i) => {
|
|
const x = n > 1 ? (i / (n - 1)) * w : 0;
|
|
const y = h - pad - (Math.max(0, Math.min(100, v)) / 100) * (h - 2 * pad);
|
|
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
})
|
|
.join(' ');
|
|
},
|
|
|
|
get cpuLine() { return this._coords(this.cpu); },
|
|
get cpuArea() { return `0,100 ${this._coords(this.cpu)} 300,100`; },
|
|
get memLine() { return this._coords(this.mem); },
|
|
get memArea() { return `0,100 ${this._coords(this.mem)} 300,100`; },
|
|
}));
|
|
|
|
// ── Command palette + keyboard shortcuts ────────────────────────────
|
|
// German nav targets (mirrors the sidebar) with a leader-key hint. Leader
|
|
// mnemonics: i=dIenste, l=Log, e=Einstellungen, y=sYstem (d/s already taken).
|
|
const CMDK_NAV = [
|
|
{ label: 'Dashboard', href: '/', hint: 'g d' },
|
|
{ label: 'Server', href: '/servers', hint: 'g s' },
|
|
{ label: 'Dienste', href: '/services', hint: 'g i' },
|
|
{ label: 'Dateien', href: '/files', hint: 'g f' },
|
|
{ label: 'Audit-Log', href: '/audit', hint: 'g l' },
|
|
{ label: 'Einstellungen', href: '/settings', hint: 'g e' },
|
|
{ label: 'System', href: '/system', hint: 'g y' },
|
|
{ label: 'Version', href: '/versions', hint: 'g v' },
|
|
];
|
|
const CMDK_ACTIONS = [
|
|
{ label: 'Server hinzufügen', action: 'create-server', hint: '' },
|
|
];
|
|
const CMDK_GO = { d: '/', s: '/servers', i: '/services', f: '/files', l: '/audit', e: '/settings', y: '/system', v: '/versions' };
|
|
|
|
window.Alpine.data('cmdk', () => ({
|
|
open: false,
|
|
help: false,
|
|
query: '',
|
|
active: 0,
|
|
leader: false,
|
|
leaderTimer: null,
|
|
nav: CMDK_NAV,
|
|
|
|
init() {
|
|
// Reset overlays after an SPA navigation. The component is recreated on each
|
|
// wire:navigate, so we MUST remove this document listener in destroy() — else
|
|
// every navigation would accumulate another permanent callback.
|
|
this._onNav = () => {
|
|
this.open = false;
|
|
this.help = false;
|
|
this.leader = false;
|
|
};
|
|
document.addEventListener('livewire:navigated', this._onNav);
|
|
},
|
|
|
|
destroy() {
|
|
document.removeEventListener('livewire:navigated', this._onNav);
|
|
},
|
|
|
|
get items() {
|
|
const q = this.query.trim().toLowerCase();
|
|
const all = [...CMDK_NAV, ...CMDK_ACTIONS];
|
|
return q === '' ? all : all.filter((i) => i.label.toLowerCase().includes(q));
|
|
},
|
|
|
|
toggle(force) {
|
|
this.open = force === true ? true : ! this.open;
|
|
if (this.open) {
|
|
this.help = false;
|
|
this.query = '';
|
|
this.active = 0;
|
|
this.$nextTick(() => this.$refs.input?.focus());
|
|
}
|
|
},
|
|
|
|
close() {
|
|
this.open = false;
|
|
this.help = false;
|
|
},
|
|
|
|
move(d) {
|
|
const n = this.items.length;
|
|
if (n) this.active = (this.active + d + n) % n;
|
|
},
|
|
|
|
run(item) {
|
|
item = item || this.items[this.active] || null;
|
|
if (! item) return;
|
|
if (item.href) {
|
|
this.navigate(item.href);
|
|
} else if (item.action === 'create-server') {
|
|
this.close();
|
|
window.Livewire?.dispatch('openModal', { component: 'modals.create-server' });
|
|
}
|
|
},
|
|
|
|
navigate(href) {
|
|
this.close();
|
|
if (window.Livewire?.navigate) window.Livewire.navigate(href);
|
|
else window.location.href = href;
|
|
},
|
|
|
|
focusSearch() {
|
|
const el = document.querySelector('[data-page-search]');
|
|
if (el) {
|
|
el.focus();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
go(key) {
|
|
if (CMDK_GO[key]) this.navigate(CMDK_GO[key]);
|
|
},
|
|
|
|
onKey(e) {
|
|
// Cmd/Ctrl-K toggles the palette even from within an input.
|
|
if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
this.toggle();
|
|
return;
|
|
}
|
|
if (this.open || this.help) return; // overlay keys handled in the markup
|
|
const t = e.target;
|
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return;
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
// Only consume "/" when a page search exists — otherwise leave the browser's quick-find.
|
|
if (e.key === '/') { if (this.focusSearch()) e.preventDefault(); return; }
|
|
if (e.key === '?') { e.preventDefault(); this.help = true; return; }
|
|
if (this.leader) {
|
|
this.leader = false;
|
|
clearTimeout(this.leaderTimer);
|
|
this.go(e.key.toLowerCase());
|
|
return;
|
|
}
|
|
if (e.key === 'g' || e.key === 'G') {
|
|
this.leader = true;
|
|
this.leaderTimer = setTimeout(() => { this.leader = false; }, 1200);
|
|
}
|
|
},
|
|
}));
|
|
});
|