First version of vuejs terminal
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<MainLayout :gap="false">
|
||||
<template #dialogs>
|
||||
</template>
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
<template #left>
|
||||
<span class="title">{{ name }}</span>
|
||||
</template>
|
||||
<template #right>
|
||||
<a style="margin-left: 20px; margin-right: 5px;" :href="'/frontend/logs.html?appId=' + id" target="_blank"><Button severity="secondary" icon="pi pi-align-left" :label="$t('logs.title')" /></a>
|
||||
<a style="margin-right: 5px;" :href="'/frontend/filemanager.html#/home/app/' + id" target="_blank"><Button type="button" severity="secondary" icon="pi pi-folder" :label="$t('filemanager.title')" /></a>
|
||||
<Button type="button" :label="$t('filemanager.toolbar.restartApp')" severity="secondary" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart"/>
|
||||
</template>
|
||||
</TopBar>
|
||||
</template>
|
||||
<template #body>
|
||||
<div id="terminal"></div>
|
||||
</template>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import superagent from 'superagent';
|
||||
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Menu from 'primevue/menu';
|
||||
import ProgressSpinner from 'primevue/progressspinner';
|
||||
|
||||
import { TopBar, MainLayout } from 'pankow';
|
||||
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
import { create } from '../models/AppModel.js';
|
||||
|
||||
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
||||
|
||||
export default {
|
||||
name: 'Terminal',
|
||||
components: {
|
||||
Button,
|
||||
Dialog,
|
||||
InputText,
|
||||
MainLayout,
|
||||
Menu,
|
||||
ProgressSpinner,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accessToken: localStorage.token,
|
||||
apiOrigin: API_ORIGIN || '',
|
||||
appModel: null,
|
||||
busyRestart: false,
|
||||
id: '',
|
||||
name: '',
|
||||
terminal: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onClear() {
|
||||
},
|
||||
async onRestartApp() {
|
||||
if (this.type !== 'app') return;
|
||||
|
||||
this.busyRestart = true;
|
||||
|
||||
await this.appModel.restart();
|
||||
|
||||
this.busyRestart = false;
|
||||
},
|
||||
async connect(retry = false) {
|
||||
document.getElementsByClassName('cloudron-layout-body')[0].innerHTML = '';
|
||||
|
||||
let execId;
|
||||
try {
|
||||
const result = await superagent.post(`${this.apiOrigin}/api/v1/apps/${this.id}/exec`).query({ access_token: this.accessToken }).send({ cmd: [ '/bin/bash' ], tty: true, lang: 'C.UTF-8' });
|
||||
execId = result.body.id;
|
||||
} catch (error) {
|
||||
console.error('Cannot create socket.', error);
|
||||
return setTimeout(() => this.connect(true), 1000);
|
||||
}
|
||||
|
||||
this.terminal = new Terminal();
|
||||
this.terminal.open(document.getElementsByClassName('cloudron-layout-body')[0]);
|
||||
|
||||
if (retry) this.terminal.writeln('Reconnecting...');
|
||||
else this.terminal.writeln('Connecting...');
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
this.terminal.loadAddon(fitAddon);
|
||||
fitAddon.fit();
|
||||
|
||||
// Let the browser handle paste
|
||||
this.terminal.attachCustomKeyEventHandler(function (e) {
|
||||
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) return false;
|
||||
});
|
||||
|
||||
// websocket cannot use relative urls
|
||||
var url = `${this.apiOrigin.replace('https', 'wss')}/api/v1/apps/${this.id}/exec/${execId}/startws?tty=true&rows=${this.terminal.rows}&columns=${this.terminal.cols}&access_token=${this.accessToken}`;
|
||||
const socket = new WebSocket(url);
|
||||
|
||||
this.terminal.loadAddon(new AttachAddon(socket));
|
||||
|
||||
socket.addEventListener('close', (event) => {
|
||||
console.log('Socket closed. Reconnecting...');
|
||||
return setTimeout(() => this.connect(true), 1000);
|
||||
});
|
||||
|
||||
socket.addEventListener('error', (error) => {
|
||||
console.log('Socket error. Reconnecting...', error);
|
||||
return setTimeout(() => this.connect(true), 1000);
|
||||
});
|
||||
|
||||
this.terminal.focus();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (!localStorage.token) {
|
||||
console.error('Set localStorage.token');
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const id = urlParams.get('id');
|
||||
|
||||
if (!id) {
|
||||
console.error('No app id specified');
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = id;
|
||||
this.name = id;
|
||||
|
||||
this.appModel = create(this.apiOrigin, this.accessToken, this.id);
|
||||
|
||||
try {
|
||||
const app = await this.appModel.get();
|
||||
this.name = `${app.label || app.fqdn} (${app.manifest.title})`;
|
||||
} catch (e) {
|
||||
console.error(`Failed to get app info for ${this.id}:`, e);
|
||||
}
|
||||
|
||||
window.document.title = `Terminal - ${this.name}`;
|
||||
|
||||
this.connect();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.cloudron-top {
|
||||
background-color: black;
|
||||
color: white;
|
||||
margin-bottom: 0 !important;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user