mirror of https://github.com/rancher/dashboard.git
497 lines
11 KiB
Vue
497 lines
11 KiB
Vue
<script>
|
|
import AnsiUp from 'ansi_up';
|
|
import { addParams } from '@shell/utils/url';
|
|
import { LOGS_TIME, LOGS_WRAP, DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
import day from 'dayjs';
|
|
import Select from '@shell/components/form/Select';
|
|
|
|
import { escapeHtml, escapeRegex } from '@shell/utils/string';
|
|
|
|
import Socket, {
|
|
EVENT_CONNECTED,
|
|
EVENT_DISCONNECTED,
|
|
EVENT_MESSAGE,
|
|
EVENT_CONNECT_ERROR
|
|
} from '@shell/utils/socket';
|
|
import Window from '@shell/components/nav/WindowManager/Window';
|
|
import { downloadFile } from '@shell/utils/download';
|
|
import ApplicationSocketMixin from './ApplicationSocketMixin';
|
|
|
|
let lastId = 1;
|
|
const ansiup = new AnsiUp();
|
|
|
|
export default {
|
|
components: {
|
|
Window,
|
|
Checkbox,
|
|
AsyncButton,
|
|
Select
|
|
},
|
|
|
|
mixins: [ApplicationSocketMixin],
|
|
|
|
props: {
|
|
ansiToHtml: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
isFollowing: true,
|
|
timestamps: this.$store.getters['prefs/get'](LOGS_TIME),
|
|
wrap: this.$store.getters['prefs/get'](LOGS_WRAP),
|
|
search: '',
|
|
lines: [],
|
|
instance: ''
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
|
|
instanceChoicesWithNone() {
|
|
return [
|
|
...this.instanceChoices,
|
|
{
|
|
label: 'No Instance Filter',
|
|
value: null
|
|
}
|
|
];
|
|
},
|
|
|
|
filtered() {
|
|
if ( !this.search && !this.instance) {
|
|
return this.lines;
|
|
}
|
|
|
|
const re = new RegExp(escapeRegex(this.search), 'img');
|
|
const out = [];
|
|
|
|
for ( const line of this.lines ) {
|
|
let msg = line.rawMsg;
|
|
|
|
if ( this.instance) {
|
|
const pod = msg.substring(1, msg.length);
|
|
|
|
if (!pod.startsWith(this.instance)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const matches = msg.match(re);
|
|
|
|
if ( !matches ) {
|
|
continue;
|
|
}
|
|
|
|
const parts = msg.split(re);
|
|
|
|
msg = '';
|
|
while ( parts.length || matches.length ) {
|
|
if ( parts.length ) {
|
|
msg += ansiup.ansi_to_html(parts.shift()); // This also escapes
|
|
}
|
|
|
|
if ( matches.length ) {
|
|
msg += `<span class="highlight">${ ansiup.ansi_to_html(matches.shift()) }</span>`;
|
|
}
|
|
}
|
|
|
|
out.push({
|
|
id: line.id,
|
|
time: line.time,
|
|
msg,
|
|
});
|
|
}
|
|
|
|
return out;
|
|
},
|
|
|
|
timeFormatStr() {
|
|
const dateFormat = escapeHtml( this.$store.getters['prefs/get'](DATE_FORMAT));
|
|
const timeFormat = escapeHtml( this.$store.getters['prefs/get'](TIME_FORMAT));
|
|
|
|
return `${ dateFormat } ${ timeFormat }`;
|
|
}
|
|
},
|
|
|
|
beforeDestroy() {
|
|
this.$refs.body.removeEventListener('scroll', this.boundUpdateFollowing);
|
|
this.cleanup();
|
|
},
|
|
|
|
async mounted() {
|
|
await this.connect();
|
|
this.boundUpdateFollowing = this.updateFollowing.bind(this);
|
|
this.$refs.body.addEventListener('scroll', this.boundUpdateFollowing);
|
|
this.boundFlush = this.flush.bind(this);
|
|
this.timerFlush = setInterval(this.boundFlush, 100);
|
|
},
|
|
|
|
methods: {
|
|
async getSocketUrl() {
|
|
const { url, token } = await this.getRootSocketUrl();
|
|
|
|
return addParams(url, { follow: true, authtoken: token });
|
|
},
|
|
|
|
async connect() {
|
|
if ( this.socket ) {
|
|
await this.socket.disconnect();
|
|
this.socket = null;
|
|
this.lines = [];
|
|
}
|
|
|
|
this.lines = [];
|
|
|
|
const url = await this.getSocketUrl();
|
|
|
|
this.socket = new Socket(url, true, 0);
|
|
this.socket.addEventListener(EVENT_CONNECTED, (e) => {
|
|
this.isOpen = true;
|
|
});
|
|
|
|
this.socket.addEventListener(EVENT_DISCONNECTED, (e) => {
|
|
this.isOpen = false;
|
|
});
|
|
|
|
this.socket.addEventListener(EVENT_CONNECT_ERROR, (e) => {
|
|
this.isOpen = false;
|
|
console.error('Connect Error', e); // eslint-disable-line no-console
|
|
});
|
|
|
|
this.socket.addEventListener(EVENT_MESSAGE, (e) => {
|
|
let parsedData;
|
|
|
|
try {
|
|
parsedData = JSON.parse(e.detail.data);
|
|
} catch (e) {
|
|
console.warn('Unable to parse websocket data: ', e.detail.data); // eslint-disable-line no-console
|
|
|
|
return;
|
|
}
|
|
|
|
const { PodName, Message } = parsedData;
|
|
|
|
const line = `[${ PodName }] ${ Message }`;
|
|
|
|
this.backlog.push({
|
|
id: lastId++,
|
|
msg: this.ansiToHtml ? ansiup.ansi_to_html(line) : line,
|
|
rawMsg: line,
|
|
// time,
|
|
});
|
|
});
|
|
|
|
this.socket.connect();
|
|
},
|
|
|
|
flush() {
|
|
if ( this.backlog.length ) {
|
|
this.lines.push(...this.backlog);
|
|
this.backlog = [];
|
|
}
|
|
|
|
if ( this.isFollowing ) {
|
|
this.$nextTick(() => {
|
|
this.follow();
|
|
});
|
|
}
|
|
},
|
|
|
|
updateFollowing() {
|
|
const el = this.$refs.body;
|
|
|
|
this.isFollowing = el.scrollTop + el.clientHeight + 2 >= el.scrollHeight;
|
|
},
|
|
|
|
clear() {
|
|
this.lines = [];
|
|
},
|
|
|
|
download(btnCb) {
|
|
const date = new Date().toISOString().split('.')[0];
|
|
const fileName = `${ this.application.nameDisplay }-${ date }`;
|
|
|
|
downloadFile(fileName, this.lines.map(l => `${ l.rawMsg }`).join('\n'))
|
|
.then(() => btnCb(true))
|
|
.catch(() => btnCb(false));
|
|
},
|
|
|
|
follow() {
|
|
const el = this.$refs.body;
|
|
|
|
el.scrollTop = el.scrollHeight;
|
|
},
|
|
|
|
toggleWrap(on) {
|
|
this.wrap = on;
|
|
this.$store.dispatch('prefs/set', { key: LOGS_WRAP, value: this.wrap });
|
|
},
|
|
|
|
format(time) {
|
|
if ( !time ) {
|
|
return '';
|
|
}
|
|
|
|
return day(time).format(this.timeFormatStr);
|
|
},
|
|
|
|
cleanup() {
|
|
if ( this.socket ) {
|
|
this.socket.disconnect();
|
|
this.socket = null;
|
|
}
|
|
|
|
clearInterval(this.timerFlush);
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Window
|
|
:active="active"
|
|
:before-close="cleanup"
|
|
class="epinio-app-log"
|
|
>
|
|
<template #title>
|
|
<div class="title-inner log-action ">
|
|
<div class="title-inner-left">
|
|
<Select
|
|
v-if="instanceChoices.length > 1"
|
|
v-model="instance"
|
|
:disabled="instanceChoices.length === 1"
|
|
class="containerPicker auto-width"
|
|
:options="instanceChoicesWithNone"
|
|
:clearable="true"
|
|
placement="top"
|
|
placeholder="Filter by Instance"
|
|
>
|
|
<template #selected-option="option">
|
|
<t
|
|
v-if="option"
|
|
k="epinio.applications.wm.containerName"
|
|
:label="option.label"
|
|
/>
|
|
</template>
|
|
</Select>
|
|
|
|
<button
|
|
class="btn bg-primary ml-5"
|
|
:disabled="isFollowing"
|
|
@click="follow"
|
|
>
|
|
<t k="wm.containerLogs.follow" />
|
|
</button>
|
|
<button
|
|
class=" btn bg-primary ml-5"
|
|
@click="clear"
|
|
>
|
|
<t k="wm.containerLogs.clear" />
|
|
</button>
|
|
<AsyncButton
|
|
class="ml-5"
|
|
mode="download"
|
|
@click="download"
|
|
/>
|
|
</div>
|
|
<div style="flex: 1;" />
|
|
<div class="title-inner-right">
|
|
<div
|
|
class="status log-action text-center p-10"
|
|
style="min-width: 80px;"
|
|
>
|
|
<t
|
|
:class="{'text-success': isOpen, 'text-error': !isOpen}"
|
|
:k="isOpen ? 'wm.connection.connected' : 'wm.connection.disconnected'"
|
|
/>
|
|
</div>
|
|
<div class="log-action ml-5">
|
|
<input
|
|
v-model="search"
|
|
class="input-sm"
|
|
type="search"
|
|
:placeholder="t('wm.containerLogs.search')"
|
|
>
|
|
</div>
|
|
<div class="log-action ml-5">
|
|
<v-popover
|
|
trigger="click"
|
|
placement="top"
|
|
>
|
|
<button class="btn bg-primary">
|
|
<i class="icon icon-gear" />
|
|
</button>
|
|
|
|
<template slot="popover">
|
|
<div class="filter-popup">
|
|
<div>
|
|
<Checkbox
|
|
:label="t('wm.containerLogs.wrap')"
|
|
:value="wrap"
|
|
@input="toggleWrap "
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</v-popover>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #body>
|
|
<div
|
|
ref="body"
|
|
:class="{'logs-container': true, 'open': isOpen, 'closed': !isOpen, 'show-times': timestamps && filtered.length, 'wrap-lines': wrap}"
|
|
>
|
|
<table
|
|
class="fixed"
|
|
cellpadding="0"
|
|
cellspacing="0"
|
|
>
|
|
<tbody class="logs-body">
|
|
<template v-if="filtered.length">
|
|
<tr
|
|
v-for="line in filtered"
|
|
:key="line.id"
|
|
>
|
|
<td
|
|
:key="line.id + '-time'"
|
|
class="time"
|
|
v-html="format(line.time)"
|
|
/>
|
|
<td
|
|
:key="line.id + '-msg'"
|
|
class="msg"
|
|
v-html="line.msg"
|
|
/>
|
|
</tr>
|
|
</template>
|
|
<tr v-else-if="search">
|
|
<td
|
|
v-t="'wm.containerLogs.noMatch'"
|
|
colspan="2"
|
|
class="msg text-muted"
|
|
/>
|
|
</tr>
|
|
<tr
|
|
v-else
|
|
v-t="'epinio.applications.wm.noData'"
|
|
colspan="2"
|
|
class="msg text-muted"
|
|
/>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
</Window>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.epinio-app-log {
|
|
.v-select.inline.vs--single.vs--open .vs__selected {
|
|
position: inherit;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss" scoped>
|
|
.title-inner {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
.title-inner {
|
|
display: flex;
|
|
flex-direction: row;
|
|
&-left, &-right {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
// .title-left {
|
|
|
|
// }
|
|
|
|
.logs-container {
|
|
height: 100%;
|
|
overflow: auto;
|
|
padding: 5px;
|
|
background-color: var(--logs-bg);
|
|
font-family: Menlo,Consolas,monospace;
|
|
color: var(--logs-text);
|
|
|
|
.closed {
|
|
opacity: 0.25;
|
|
}
|
|
|
|
.time {
|
|
white-space: nowrap;
|
|
display: none;
|
|
width: 0;
|
|
padding-right: 15px;
|
|
user-select: none;
|
|
}
|
|
|
|
&.show-times .time {
|
|
display: initial;
|
|
width: auto;
|
|
}
|
|
|
|
.msg {
|
|
white-space: nowrap;
|
|
|
|
.highlight {
|
|
color: var(--logs-highlight);
|
|
background-color: var(--logs-highlight-bg);
|
|
}
|
|
}
|
|
|
|
&.wrap-lines .msg {
|
|
white-space: normal;
|
|
}
|
|
}
|
|
|
|
.containerPicker {
|
|
::v-deep &.unlabeled-select {
|
|
display: inline-block;
|
|
min-width: 200px;
|
|
height: 30px;
|
|
min-height: 30px;
|
|
width: initial;
|
|
}
|
|
}
|
|
|
|
.log-action {
|
|
button {
|
|
border: 0 !important;
|
|
min-height: 30px;
|
|
line-height: 30px;
|
|
}
|
|
|
|
> input {
|
|
height: 30px;
|
|
}
|
|
}
|
|
|
|
.status {
|
|
align-items: center;
|
|
display: flex;
|
|
min-width: 80px;
|
|
height: 30px;
|
|
}
|
|
|
|
.filter-popup {
|
|
> * {
|
|
margin-bottom: 10px;
|
|
}
|
|
}
|
|
|
|
.title-left {
|
|
display: flex;
|
|
}
|
|
</style>
|