Initial commit
This commit is contained in:
5
ui/widgets/busy.svg
Normal file
5
ui/widgets/busy.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="150" height="150" viewBox="0 0 26 26">
|
||||
<path transform="scale(.26)" d="M21.041 34.48c-.026 0-.056.005-.082.008a.716.716 0 0 0-.232.074L6.092 42.113a.698.698 0 0 0-.375.631V44.6a.2.2 0 0 0 0 .007.695.695 0 0 0 .367.608l13.297 7.347a.262.262 0 0 1 .135.233v7.242c.003.384.322.7.707.697h.834a.703.703 0 0 0 .691-.697v-8.055c0-.162.107-.261.27-.261h7.894c.162 0 .264.1.264.261v8.055c.003.384.323.7.707.697h.832a.705.705 0 0 0 .691-.697V45.605c0-.11.024-.155.053-.187a.292.292 0 0 1 .158-.076.3.3 0 0 1 .18.008c.036.015.072.048.113.129.004.007.004.004.008.013a.2.2 0 0 0 0 .008l6.092 13.334c.124.27.39.418.646.414a.703.703 0 0 0 .631-.436l3.838-9.54c.06-.15.129-.165.256-.165s.188.015.248.164l3.832 9.542c.109.283.37.444.63.45a.71.71 0 0 0 .661-.421L55.836 45.5c.046-.1.083-.126.121-.143a.278.278 0 0 1 .172-.007.292.292 0 0 1 .158.076c.03.032.052.077.053.187v14.424a.2.2 0 0 0 0 .008.703.703 0 0 0 .707.69h.834a.696.696 0 0 0 .697-.69.2.2 0 0 0 0-.008V43.795a.696.696 0 0 0-.697-.691H55.01a.695.695 0 0 0-.639.398.2.2 0 0 0-.006 0c-1.688 3.661-3.369 7.33-5.056 10.99-.065.141-.132.161-.256.158-.123-.003-.182-.03-.24-.173l-3.786-9.329a.7.7 0 0 0-.646-.45.71.71 0 0 0-.654.45l-3.793 9.329c-.06.145-.118.17-.24.173-.125.003-.184-.017-.249-.158l-5.054-10.99a.705.705 0 0 0-.639-.398h-2.87a.702.702 0 0 0-.706.69v5.41c0 .161-.107.278-.264.278h-7.894c-.158 0-.27-.117-.27-.279v-5.408a.698.698 0 0 0-.691-.691h-.834a.702.702 0 0 0-.707.69v5.741c0 .11-.078.261-.172.33-.114.084-.13.087-.211.045a.2.2 0 0 0-.008 0 .2.2 0 0 0-.03-.015.2.2 0 0 0-.023-.008c-3.673-1.892-7.333-3.954-11.011-5.889a.276.276 0 0 1-.135-.166.91.91 0 0 1 .008-.277.61.61 0 0 1 .142-.203.694.694 0 0 1 .104-.075c.049-.014.1-.035.144-.06 4.332-2.275 8.706-4.465 13.047-6.71a.702.702 0 0 0 .377-.622v-.699a.703.703 0 0 0-.707-.706zm40.654 8.624a.705.705 0 0 0-.697.707v16.224a.71.71 0 0 0 .705.7h.826a.704.704 0 0 0 .7-.7v-8.053c0-.16.109-.261.271-.261h6.887a.702.702 0 0 0 .699-.707v-.842a.698.698 0 0 0-.7-.69H63.5c-.157 0-.271-.118-.271-.279v-3.598c0-.16.109-.263.271-.263h12.334c.162 0 .264.102.264.263v14.43c.003.38.31.695.69.7a.2.2 0 0 0 .007 0h.826a.709.709 0 0 0 .707-.7v-14.43c0-.162.102-.263.264-.263h4.543c.094 0 .177.043.226.12l4.846 7.686a.25.25 0 0 1 .045.135v6.752a.707.707 0 0 0 .691.7h.832a.707.707 0 0 0 .692-.7v-6.752c0-.044.014-.086.045-.135l5.642-8.97c.289-.456-.046-1.072-.586-1.074h-.955a.7.7 0 0 0-.6.324l-4.433 7.031c-.064.101-.145.135-.225.135-.08 0-.16-.034-.224-.135l-4.44-7.031a.696.696 0 0 0-.586-.324c-7.465 0-14.936-.004-22.402 0h-.008zM4.447 47.303c-.022 0-.046.005-.068.008a.696.696 0 0 0-.639.69v.812a.713.713 0 0 0 .37.617l10.816 5.949.008.008a.2.2 0 0 0 .023.015.2.2 0 0 0 .03.022c.093.05.142.127.142.226v1.23c0 .1-.047.186-.135.235a.2.2 0 0 0-.045.03l-10.847 6.22a.7.7 0 0 0-.362.608v.85a.2.2 0 0 0 0 .007c.004.528.597.858 1.053.592l11.898-6.887a.699.699 0 0 0 .346-.61v-3.267a.703.703 0 0 0-.361-.61c-3.964-2.217-11.89-6.655-11.89-6.655a.2.2 0 0 0-.009 0 .689.689 0 0 0-.33-.09z" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.8" stroke="#e9a" stroke-dasharray="20 20 20 20 20" stroke-dashoffset="-50">
|
||||
<animate begin="0s" attributeName="stroke-dashoffset" from="-100" to="100" dur="5s" repeatCount="indefinite" restart="always"/>
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
316
ui/widgets/chart.vue
Normal file
316
ui/widgets/chart.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<slot />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint vue/attribute-hyphenation: 0 */
|
||||
|
||||
const XMLNG = "http://www.w3.org/2000/svg";
|
||||
const XMLNS = "http://www.w3.org/2000/xmlns/";
|
||||
const XMLNGLink = "http://www.w3.org/1999/xlink";
|
||||
|
||||
class Data {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.max = this.getMax(data);
|
||||
}
|
||||
|
||||
setMax(max) {
|
||||
this.max = this.max > max ? this.max : max;
|
||||
}
|
||||
|
||||
getMax(data) {
|
||||
let max = 0;
|
||||
|
||||
for (let i in data) {
|
||||
if (data[i] <= max) {
|
||||
continue;
|
||||
}
|
||||
|
||||
max = data[i];
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
class BaseDrawer {
|
||||
constructor() {
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
toCellHeight(cellHeight, data, n) {
|
||||
if (data.max === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (cellHeight / data.max) * n;
|
||||
}
|
||||
|
||||
toBottomHeight(cellHeight, n) {
|
||||
return cellHeight - n;
|
||||
}
|
||||
|
||||
cellWidth(rootDim, data) {
|
||||
return rootDim.width / data.data.length;
|
||||
}
|
||||
|
||||
createEl(parent, tag, properties) {
|
||||
let np = document.createElementNS(XMLNG, tag);
|
||||
|
||||
for (let p in properties) {
|
||||
if (p.indexOf("xlink:") === 0) {
|
||||
np.setAttributeNS(XMLNGLink, p, properties[p]);
|
||||
} else if (p.indexOf("xmlns:") === 0) {
|
||||
np.setAttributeNS(XMLNS, p, properties[p]);
|
||||
} else {
|
||||
np.setAttribute(p, properties[p]);
|
||||
}
|
||||
}
|
||||
|
||||
parent.appendChild(np);
|
||||
|
||||
this.elements.push(np);
|
||||
|
||||
return np;
|
||||
}
|
||||
|
||||
removeAllEl(parent) {
|
||||
for (let i in this.elements) {
|
||||
parent.removeChild(this.elements[i]);
|
||||
}
|
||||
|
||||
this.elements = [];
|
||||
}
|
||||
|
||||
draw(parent, rootDim, data) {}
|
||||
}
|
||||
|
||||
class BarDrawer extends BaseDrawer {
|
||||
constructor(topBottomPadding) {
|
||||
super();
|
||||
|
||||
this.topBottomPadding = topBottomPadding;
|
||||
}
|
||||
|
||||
draw(parent, rootDim, data) {
|
||||
let cellWidth = this.cellWidth(rootDim, data),
|
||||
currentWidth = cellWidth / 2,
|
||||
cellHalfHeight = rootDim.height - this.topBottomPadding / 2,
|
||||
cellHeight = rootDim.height - this.topBottomPadding;
|
||||
|
||||
for (let i in data.data) {
|
||||
let h = this.toCellHeight(cellHeight, data, data.data[i]);
|
||||
|
||||
this.createEl(parent, "path", {
|
||||
d:
|
||||
"M" +
|
||||
currentWidth +
|
||||
"," +
|
||||
Math.round(this.toBottomHeight(cellHalfHeight, h)) +
|
||||
" L" +
|
||||
currentWidth +
|
||||
"," +
|
||||
cellHalfHeight,
|
||||
class: h > 0 ? "" : "zero"
|
||||
});
|
||||
|
||||
currentWidth += cellWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UpsideDownBarDrawer extends BarDrawer {
|
||||
draw(parent, rootDim, data) {
|
||||
let cellWidth = this.cellWidth(rootDim, data),
|
||||
currentWidth = cellWidth / 2,
|
||||
padHalfHeight = this.topBottomPadding / 2,
|
||||
cellHeight = rootDim.height - this.topBottomPadding;
|
||||
|
||||
for (let i in data.data) {
|
||||
let h = this.toCellHeight(cellHeight, data, data.data[i]);
|
||||
|
||||
this.createEl(parent, "path", {
|
||||
d:
|
||||
"M" +
|
||||
currentWidth +
|
||||
"," +
|
||||
padHalfHeight +
|
||||
" L" +
|
||||
currentWidth +
|
||||
"," +
|
||||
(Math.round(h) + padHalfHeight),
|
||||
class: h > 0 ? "" : "zero"
|
||||
});
|
||||
|
||||
currentWidth += cellWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Chart {
|
||||
constructor(el, width, height, drawer) {
|
||||
this.el = el;
|
||||
this.drawer = drawer;
|
||||
this.group = null;
|
||||
this.paths = [];
|
||||
this.dim = { width, height };
|
||||
|
||||
this.el.setAttribute(
|
||||
"viewBox",
|
||||
"0 0 " +
|
||||
parseInt(this.dim.width, 10) +
|
||||
" " +
|
||||
parseInt(this.dim.height, 10)
|
||||
);
|
||||
|
||||
this.el.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
||||
}
|
||||
|
||||
getGroupRoot() {
|
||||
if (this.group) {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
this.group = document.createElementNS(XMLNG, "g");
|
||||
|
||||
this.el.appendChild(this.group);
|
||||
|
||||
return this.group;
|
||||
}
|
||||
|
||||
draw(data, manualMax) {
|
||||
let d = new Data(data);
|
||||
let max = d.max;
|
||||
|
||||
d.setMax(manualMax);
|
||||
|
||||
this.drawer.removeAllEl(this.getGroupRoot());
|
||||
this.drawer.draw(this.getGroupRoot(), this.dim, d);
|
||||
|
||||
return {
|
||||
dataMax: max,
|
||||
resultMax: d.max
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.drawer.removeAllEl();
|
||||
this.el.removeChild(this.getGroupRoot());
|
||||
}
|
||||
}
|
||||
|
||||
function buildDrawer(type) {
|
||||
switch (type) {
|
||||
case "Bar":
|
||||
return new BarDrawer(10);
|
||||
|
||||
case "UpsideDownBar":
|
||||
return new UpsideDownBarDrawer(10);
|
||||
}
|
||||
|
||||
return new Error("Undefined drawer: " + type);
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
values: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
previousMax: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
values() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
max() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
},
|
||||
enabled(newVal) {
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draw();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.chart = new Chart(
|
||||
this.$el,
|
||||
this.width,
|
||||
this.height,
|
||||
buildDrawer(this.type)
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.chart.clear();
|
||||
},
|
||||
methods: {
|
||||
draw() {
|
||||
let r = this.chart.draw(this.values, this.max);
|
||||
|
||||
if (r.dataMax === this.previousMax) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("max", r.dataMax);
|
||||
this.previousMax = r.dataMax;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
109
ui/widgets/connect.css
Normal file
109
ui/widgets/connect.css
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
left: 159px;
|
||||
display: none;
|
||||
background: #333;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
#connect .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#connect:before {
|
||||
left: 30px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#connect {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#connect:before {
|
||||
left: 149px;
|
||||
}
|
||||
}
|
||||
|
||||
#connect.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#connect h1 {
|
||||
padding: 15px 15px 0 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#connect-close {
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
#connect-busy-overlay {
|
||||
z-index: 2;
|
||||
background: #2229 url("busy.svg") center center no-repeat;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
||||
|
||||
#connect-warning {
|
||||
padding: 10px;
|
||||
font-size: 0.85em;
|
||||
background: #b44;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-warning-icon {
|
||||
float: left;
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#connect-warning-icon::after {
|
||||
background: #c55;
|
||||
}
|
||||
|
||||
#connect-warning-msg {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#connect-warning-msg p {
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
#connect-warning-msg a {
|
||||
color: #faa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
152
ui/widgets/connect.vue
Normal file
152
ui/widgets/connect.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="connect"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<h1 class="window-title">Establish connection with</h1>
|
||||
|
||||
<slot v-if="inputting"></slot>
|
||||
|
||||
<connect-switch
|
||||
v-if="!inputting"
|
||||
:knowns-length="knowns.length"
|
||||
:tab="tab"
|
||||
@switch="switchTab"
|
||||
></connect-switch>
|
||||
|
||||
<connect-new
|
||||
v-if="tab === 'new' && !inputting"
|
||||
:connectors="connectors"
|
||||
@select="selectConnector"
|
||||
></connect-new>
|
||||
|
||||
<connect-known
|
||||
v-if="tab === 'known' && !inputting"
|
||||
:knowns="knowns"
|
||||
:launcher-builder="knownsLauncherBuilder"
|
||||
@select="selectKnown"
|
||||
@remove="removeKnown"
|
||||
></connect-known>
|
||||
|
||||
<div id="connect-warning">
|
||||
<span id="connect-warning-icon" class="icon icon-warning1"></span>
|
||||
<div id="connect-warning-msg">
|
||||
<p>
|
||||
<strong>An insecured service may steal your secrects.</strong>
|
||||
Always exam the safty of the service before using it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Sshwifty is a free software, you can deploy it on your own trusted
|
||||
infrastructure.
|
||||
<a href="https://github.com/niruix/sshwifty" target="_blank"
|
||||
>Learn more</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="busy" id="connect-busy-overlay"></div>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import ConnectSwitch from "./connect_switch.vue";
|
||||
import ConnectKnown from "./connect_known.vue";
|
||||
import ConnectNew from "./connect_new.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
"connect-switch": ConnectSwitch,
|
||||
"connect-known": ConnectKnown,
|
||||
"connect-new": ConnectNew
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
inputting: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
knownsLauncherBuilder: {
|
||||
type: Function,
|
||||
default: () => []
|
||||
},
|
||||
connectors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
busy: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: "new",
|
||||
canSelect: true
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
switchTab(to) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tab = to;
|
||||
},
|
||||
selectConnector(connector) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("connector-select", connector);
|
||||
},
|
||||
selectKnown(known) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("known-select", known);
|
||||
},
|
||||
removeKnown(uid) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("known-remove", uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
111
ui/widgets/connect_known.css
Normal file
111
ui/widgets/connect_known.css
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-known-list {
|
||||
min-height: 200px;
|
||||
font-size: 0.75em;
|
||||
padding: 15px;
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
#connect-known-list li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt {
|
||||
display: none;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link {
|
||||
background: #287;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link:after {
|
||||
content: "\02936";
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.del {
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li:hover .labels > .opt,
|
||||
#connect-known-list li:focus .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#connect-known-list li h2 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list li h2::before {
|
||||
content: ">_";
|
||||
color: #555;
|
||||
font-size: 0.8em;
|
||||
margin-right: 5px;
|
||||
font-weight: normal;
|
||||
}
|
||||
141
ui/widgets/connect_known.vue
Normal file
141
ui/widgets/connect_known.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="connect-known-list">
|
||||
<ul class="lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span class="type" :style="'background-color: ' + known.data.color">
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
<a
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
</div>
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h2 :title="known.data.title">{{ known.data.title }}</h2>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_known.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
launcherBuilder: {
|
||||
type: Function,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
knownList: [],
|
||||
busy: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
knowns(newVal) {
|
||||
this.reload(newVal);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.reload(this.knowns);
|
||||
},
|
||||
methods: {
|
||||
reload(knownList) {
|
||||
this.knownList = [];
|
||||
|
||||
for (let i in knownList) {
|
||||
this.knownList.unshift({
|
||||
data: knownList[i],
|
||||
copying: false,
|
||||
copyStatus: "Copy link"
|
||||
});
|
||||
}
|
||||
},
|
||||
select(known) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("select", known);
|
||||
},
|
||||
async launcher(known, ev) {
|
||||
if (known.copying || this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
|
||||
this.busy = true;
|
||||
known.copying = true;
|
||||
known.copyStatus = "Copying";
|
||||
|
||||
let lnk = this.launcherBuilder(known.data);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(lnk);
|
||||
|
||||
(() => {
|
||||
known.copyStatus = "Copied!";
|
||||
})();
|
||||
} catch (e) {
|
||||
(() => {
|
||||
known.copyStatus = "Failed";
|
||||
ev.target.setAttribute("href", lnk);
|
||||
})();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
known.copyStatus = "Copy link";
|
||||
known.copying = false;
|
||||
}, 2000);
|
||||
|
||||
this.busy = false;
|
||||
},
|
||||
remove(uid) {
|
||||
if (this.busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("remove", uid);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
58
ui/widgets/connect_new.css
Normal file
58
ui/widgets/connect_new.css
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-new {
|
||||
min-height: 200px;
|
||||
background: #3a3a3a;
|
||||
font-size: 0.75em;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:hover {
|
||||
background: #544;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:active {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap {
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#connect-new li h2 {
|
||||
color: #e9a;
|
||||
}
|
||||
|
||||
#connect-new li h2::before {
|
||||
content: ">";
|
||||
margin: 0 5px 0 0;
|
||||
color: #555;
|
||||
font-weight: normal;
|
||||
transition: ease 0.3s margin;
|
||||
}
|
||||
|
||||
#connect-new li .lst-wrap:hover h2::before {
|
||||
content: ">";
|
||||
margin: 0 3px 0 2px;
|
||||
}
|
||||
53
ui/widgets/connect_new.vue
Normal file
53
ui/widgets/connect_new.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="connect-new">
|
||||
<ul class="lst1">
|
||||
<li
|
||||
v-for="(connector, ck) in connectors"
|
||||
:key="ck"
|
||||
@click="select(connector)"
|
||||
>
|
||||
<div class="lst-wrap">
|
||||
<h2 :style="'color: ' + connector.color()">{{ connector.name() }}</h2>
|
||||
{{ connector.description() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_new.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
connectors: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
select(connector) {
|
||||
this.$emit("select", connector);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
56
ui/widgets/connect_switch.css
Normal file
56
ui/widgets/connect_switch.css
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connect-switch {
|
||||
font-size: 0.88em;
|
||||
color: #aaa;
|
||||
clear: both;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
#connect-switch li .label {
|
||||
padding: 2px 7px;
|
||||
margin-left: 3px;
|
||||
font-size: 0.85em;
|
||||
background: #444;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#connect-switch li.active {
|
||||
border-color: #555;
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
#connect-switch li.active .label {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
#connect-switch li.disabled {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#connect-switch.red {
|
||||
border-color: #a56;
|
||||
}
|
||||
|
||||
#connect-switch.red li.active {
|
||||
border-color: #a56;
|
||||
}
|
||||
65
ui/widgets/connect_switch.vue
Normal file
65
ui/widgets/connect_switch.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul id="connect-switch" class="tab2" :class="{ red: tab === 'known' }">
|
||||
<li :class="{ active: tab === 'new' }" @click="switchTab('new')">
|
||||
New remote
|
||||
</li>
|
||||
|
||||
<li
|
||||
:class="{ active: tab === 'known', disabled: knownsLength <= 0 }"
|
||||
@click="knownsLength > 0 && switchTab('known')"
|
||||
>
|
||||
Known remotes
|
||||
<span v-if="knownsLength > 0" class="label">{{ knownsLength }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connect_switch.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
tab: {
|
||||
type: String,
|
||||
default: "new"
|
||||
},
|
||||
knownsLength: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
knownsLength(newVal) {
|
||||
if (newVal > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchTab("new");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(to) {
|
||||
this.$emit("switch", to);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
141
ui/widgets/connecting.svg
Normal file
141
ui/widgets/connecting.svg
Normal file
@@ -0,0 +1,141 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 121.708 21.167">
|
||||
<g transform="translate(-.748 -275.833)">
|
||||
<g transform="translate(-75.306 -116.7)">
|
||||
<rect ry="2.064" y="396.011" x="90.919" height="9.647" width="12.406" fill="none" stroke="gray" stroke-width=".882"/>
|
||||
<rect ry="1.621" y="397.954" x="93.361" height="8.7" width="11.319" fill="#262626"/>
|
||||
<rect ry="1.861" y="397.092" x="92.235" height="8.7" width="11.319" fill="#d18b8d"/>
|
||||
<g transform="translate(1.455)" fill="#fff">
|
||||
<circle cx="99.925" cy="404.292" r=".801">
|
||||
<animate id="r2e11" begin="5s;r2e12.end+5s" attributeName="r" from=".801" to="0" dur="0.1s" calcMode="paced"/>
|
||||
<animate id="r2e12" begin="r2e11.end" attributeName="r" from="0" to=".801" dur="0.1s" calcMode="paced"/>
|
||||
</circle>
|
||||
<circle cx="93.956" cy="404.292" r=".801">
|
||||
<animate begin="r2e11.begin" attributeName="r" from=".801" to="0" dur="0.1s" calcMode="paced"/>
|
||||
<animate begin="r2e12.begin" attributeName="r" from="0" to=".801" dur="0.1s" calcMode="paced"/>
|
||||
</circle>
|
||||
<rect width="2.062" height=".677" x="95.8" y="404.047" ry=".338"/>
|
||||
</g>
|
||||
<g transform="translate(1.455)">
|
||||
<rect width="7.039" height="4.755" x="93.352" y="398.217" ry=".988" fill="#ed9499"/>
|
||||
<rect width="6.262" height="4.021" x="94.073" y="398.948" ry=".835" fill="#cc7f80"/>
|
||||
<g transform="translate(-.023 .21)" fill="#f5d0d0">
|
||||
<rect ry=".185" y="399.571" x="94.849" height=".369" width="4.864"/>
|
||||
<rect ry=".185" y="400.538" x="94.849" height=".369" width="4.864"/>
|
||||
<rect ry=".185" y="401.506" x="94.849" height=".369" width="4.864"/>
|
||||
</g>
|
||||
</g>
|
||||
<animateMotion id="r2ud1" begin="0s;r2ud2.end" from="0,0" to="0,1" dur="1s" calcMode="paced"/>
|
||||
<animateMotion id="r2ud2" begin="r2ud1.end" from="0,1" to="0,0" dur="1s" calcMode="paced"/>
|
||||
</g>
|
||||
<rect width="10" height="1.2" x="17" y="295" ry="1" fill="#262626">
|
||||
<animate id="r2ssw1" begin="0s;r2ssw2.end" attributeName="width" from="10" to="11" dur="1s" calcMode="paced"/>
|
||||
<animate id="r2ssw2" begin="r2ssw1.end" attributeName="width" from="11" to="10" dur="1s" calcMode="paced"/>
|
||||
<animate id="r2ssx1" begin="0s;r2ssx2.end" attributeName="x" from="17" to="16.5" dur="1s" calcMode="paced"/>
|
||||
<animate id="r2ssx2" begin="r2ssx1.end" attributeName="x" from="16.5" to="17" dur="1s" calcMode="paced"/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="translate(-.748 -275.833)">
|
||||
<g>
|
||||
<g transform="translate(-76.79 -117.665)">
|
||||
<rect width="12.171" height="15.081" x="134.651" y="394.724" ry="3.796" fill="#262626"/>
|
||||
<rect width="10.909" height="13.555" x="134.566" y="394.751" ry="2.959" fill="none" stroke="gray" stroke-width=".8"/>
|
||||
<path d="M60.99 282.131a2.308 2.308 0 1 1 2.308 2.308v3.236" fill="none" stroke="#d18b8d" stroke-width="1.058" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="25.4000003,25.4000003"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(-76.79 -117.665)">
|
||||
<g fill="#d18b8d">
|
||||
<rect width="4.491" height="1.852" x="137.861" y="404.937" ry=".926"/>
|
||||
<circle cx="136.63" cy="405.863" r=".926"/>
|
||||
</g>
|
||||
<rect width="6.648" height="6.35" x="135.704" y="395.855" ry="2.052" fill="#d18b8d"/>
|
||||
<g>
|
||||
<rect ry=".573" y="397" x="136.697" height="2" width="1.146" fill="#f9f9f9">
|
||||
<animate id="r1e21" begin="r1lr1.begin" attributeName="height" from="2" to="1.3" dur="0.2s" fill="freeze"/>
|
||||
<animate begin="r1e21.begin" attributeName="y" from="397" to="397.7" dur="0.2s" fill="freeze"/>
|
||||
|
||||
<animate id="r1e22" begin="r1lr2.begin" attributeName="height" from="1.3" to="2" dur="0.2s" fill="freeze"/>
|
||||
<animate begin="r1e22.begin" attributeName="y" from="397.7" to="397" dur="0.2s" fill="freeze"/>
|
||||
</rect>
|
||||
<rect ry=".573" y="397.7" x="139.492" height="1.3" width="1.146" fill="#f9f9f9">
|
||||
<animate id="r1e11" begin="r1lr1.begin" attributeName="height" from="1.3" to="2" dur="0.2s" fill="freeze"/>
|
||||
<animate begin="r1e11.begin" attributeName="y" from="397.7" to="397" dur="0.2s" fill="freeze"/>
|
||||
|
||||
<animate id="r1e12" begin="r1lr2.begin" attributeName="height" from="2" to="1.3" dur="0.2s" fill="freeze"/>
|
||||
<animate begin="r1e12.begin" attributeName="y" from="397" to="397.7" dur="0.2s" fill="freeze"/>
|
||||
</rect>
|
||||
|
||||
<animateMotion begin="r1lr1.begin" from="0,0" to="1,0" dur="0.2s" calcMode="paced" fill="freeze"/>
|
||||
<animateMotion begin="r1lr2.begin" from="1,0" to="0,0" dur="0.2s" calcMode="paced" fill="freeze"/>
|
||||
</g>
|
||||
<rect width="6.648" height=".865" x="135.704" y="403.138" ry=".433" fill="gray"/>
|
||||
|
||||
<animateMotion id="r1lr1" begin="r2tor1.end" from="0,0" to="2,0" dur="0.2s" calcMode="paced" fill="freeze"/>
|
||||
<animateMotion id="r1lr2" begin="r1tor3.end" from="2,0" to="0,0" dur="0.2s" calcMode="paced" fill="freeze"/>
|
||||
</g>
|
||||
|
||||
<animateMotion id="r1ud2" begin="0s;r1ud1.end" from="0,1" to="0,0" dur="1.2s" calcMode="paced"/>
|
||||
<animateMotion id="r1ud1" begin="r1ud2.end" from="0,0" to="0,1" dur="1.2s" calcMode="paced"/>
|
||||
</g>
|
||||
<rect width="9" height="1.2" x="59" y="295" ry="1" fill="#262626">
|
||||
<animate id="r1ssw1" begin="r1ssw2.end" attributeName="width" from="9" to="10" dur="1.2s" calcMode="paced"/>
|
||||
<animate id="r1ssw2" begin="0s;r1ssw1.end" attributeName="width" from="10" to="9" dur="1.2s" calcMode="paced"/>
|
||||
<animate id="r1ssx1" begin="r1ssx2.end" attributeName="x" from="59" to="58.5" dur="1.2s" calcMode="paced"/>
|
||||
<animate id="r1ssx2" begin="0s;r1ssx1.end" attributeName="x" from="58.5" to="59" dur="1.2s" calcMode="paced"/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="translate(-.748 -275.833)">
|
||||
<g>
|
||||
<g transform="translate(-75.306 -119.101)">
|
||||
<rect width="8.852" height="9.42" x="174.008" y="399.075" ry="1.492" fill="#262626"/>
|
||||
<rect width="8.008" height="8.522" x="173.653" y="398.845" ry=".976" fill="gray" stroke="gray" stroke-width=".628"/>
|
||||
<rect width="7.01" height="7.46" x="173.878" y="399.387" ry=".854" fill="#262626"/>
|
||||
</g>
|
||||
<g transform="translate(-75.306 -119.101)">
|
||||
<rect width="6.215" height="6.614" x="174.463" y="400.102" ry=".757" fill="#848484"/>
|
||||
<g fill="#fff">
|
||||
<rect ry=".278" y="401.302" x="175.442" height=".889" width="1.169">
|
||||
<animate id="r3e11" begin="4s;r3e12.end+4s" attributeName="width" from="1.169" to="1.769" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
<animate id="r3e12" begin="r3e11.end+4s" attributeName="width" from="1.769" to="1.169" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
</rect>
|
||||
<rect ry=".278" y="401.302" x="177.29" height=".889" width="2.1">
|
||||
<animate begin="r3e11.begin" attributeName="width" from="2.1" to="1.5" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
<animate begin="r3e12.begin" attributeName="width" from="1.5" to="2.1" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
|
||||
<animate begin="r3e11.begin" attributeName="x" from="177.29" to="177.89" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
<animate begin="r3e12.begin" attributeName="x" from="177.89" to="177.29" dur="0.3s" calcMode="paced" fill="freeze"/>
|
||||
</rect>
|
||||
</g>
|
||||
<circle r="1.485" cy="404.653" cx="176.998" fill="#262626"/>
|
||||
<circle r=".887" cy="404.635" cx="176.998" fill="#d18b8d"/>
|
||||
<circle r="1.23" cy="404.509" cx="176.817" fill="none" stroke="#aaa" stroke-width=".429"/>
|
||||
</g>
|
||||
|
||||
<animateMotion id="r3ud2" begin="r3ud1.end" from="0,1" to="0,0" dur="1.3s" calcMode="paced"/>
|
||||
<animateMotion id="r3ud1" begin="0s;r3ud2.end" from="0,0" to="0,1" dur="1.3s" calcMode="paced"/>
|
||||
</g>
|
||||
<rect width="7" height="1.2" x="99" y="295" ry="1" fill="#262626">
|
||||
<animate id="r3ssw1" begin="r3ssw2.end" attributeName="width" from="7" to="8" dur="1.3s" calcMode="paced"/>
|
||||
<animate id="r3ssw2" begin="0s;r3ssw1.end" attributeName="width" from="8" to="7" dur="1.3s" calcMode="paced"/>
|
||||
<animate id="r3ssx1" begin="r3ssx2.end" attributeName="x" from="99" to="98.5" dur="1.3s" calcMode="paced"/>
|
||||
<animate id="r3ssx2" begin="0s;r3ssx1.end" attributeName="x" from="98.5" to="99" dur="1.3s" calcMode="paced"/>
|
||||
</rect>
|
||||
</g>
|
||||
<g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.8">
|
||||
<g stroke-dasharray="20 20 20 20 20 100" stroke-dashoffset="-100">
|
||||
<path d="M30.13 11.531s5.576-3.02 12.455-3.02c6.88 0 12.456 3.02 12.456 3.02" stroke="#d18b8d">
|
||||
<animate id="r1tor2" begin="r1lr2.end" attributeName="stroke-dashoffset" from="-100" to="100" dur="1.5s"/>
|
||||
</path>
|
||||
<path d="M55.04 8.514s-5.576 3.02-12.455 3.02c-6.879 0-12.455-3.02-12.455-3.02" stroke="gray">
|
||||
<animate id="r2tor1" begin="0s;r1tor2.end" attributeName="stroke-dashoffset" from="-100" to="100" dur="1.5s"/>
|
||||
</path>
|
||||
</g>
|
||||
<g stroke-dasharray="10 20 10 20 10 20 10 100" stroke-dashoffset="-100">
|
||||
<path d="M71.24 9.743h16.196a1.897 1.897 0 1 0 0-3.795 1.897 1.897 0 0 0 0 3.795h8.023" stroke="#d18b8d">
|
||||
<animate id="r1tor3" begin="r3tor1.end" attributeName="stroke-dashoffset" from="-100" to="100" dur="1.5s"/>
|
||||
</path>
|
||||
<path d="M 96.192792,9.7509803 H 72.003172" stroke="gray">
|
||||
<animate id="r3tor1" begin="r1lr1.end" attributeName="stroke-dashoffset" from="-100" to="100" dur="1.5s"/>
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
124
ui/widgets/connector.css
Normal file
124
ui/widgets/connector.css
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#connector {
|
||||
padding: 0 20px 40px 20px;
|
||||
}
|
||||
|
||||
#connector-cancel {
|
||||
text-decoration: none;
|
||||
color: #e9a;
|
||||
}
|
||||
|
||||
#connector-cancel.disabled {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
#connector-cancel::before {
|
||||
content: "\000AB";
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#connector-title {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#connector-title > h2 {
|
||||
color: #e9a;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
#connector-title.big {
|
||||
margin: 50px 0;
|
||||
}
|
||||
|
||||
#connector-title.big > h2 {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#connector-fields {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#connector-continue {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#connector-proccess {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
#connector-proccess-message {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
#connector-proccess-message > h2 {
|
||||
font-weight: normal;
|
||||
margin: 10px 0;
|
||||
color: #e9a;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#connector-proccess-message > h2 > span {
|
||||
padding: 2px 10px;
|
||||
border: 2px solid transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes connector-proccess-message-alert {
|
||||
0% {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
outline: 2px solid #e9a;
|
||||
}
|
||||
|
||||
60% {
|
||||
border-color: #e9a;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
#connector-proccess-message.alert > h2 > span {
|
||||
outline: 2px solid transparent;
|
||||
animation-name: connector-proccess-message-alert;
|
||||
animation-duration: 1.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: steps(1, end);
|
||||
}
|
||||
|
||||
#connector-proccess-indicater {
|
||||
width: 100%;
|
||||
margin: 20px auto;
|
||||
padding: 0;
|
||||
}
|
||||
463
ui/widgets/connector.vue
Normal file
463
ui/widgets/connector.vue
Normal file
@@ -0,0 +1,463 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<form
|
||||
id="connector"
|
||||
class="form1"
|
||||
action="javascript:;"
|
||||
method="POST"
|
||||
@submit="submitAndGetNext"
|
||||
>
|
||||
<a
|
||||
id="connector-cancel"
|
||||
href="javascript:;"
|
||||
:class="{ disabled: working || cancelled }"
|
||||
@click="cancel()"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="!working"
|
||||
id="connector-title"
|
||||
:class="{ big: current.fields.length <= 0 }"
|
||||
>
|
||||
<h2>{{ current.title || connector.name }}</h2>
|
||||
|
||||
<p>{{ current.message || connector.description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="working" id="connector-proccess">
|
||||
<img id="connector-proccess-indicater" src="./connecting.svg" />
|
||||
|
||||
<div id="connector-proccess-message" :class="{ alert: current.alert }">
|
||||
<h2>
|
||||
<span>{{ current.title || connector.name }}</span>
|
||||
</h2>
|
||||
|
||||
<p>{{ current.message || connector.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset id="connector-fields">
|
||||
<div
|
||||
v-for="(field, key) in current.fields"
|
||||
:key="key"
|
||||
class="field"
|
||||
:class="{ error: field.error.length > 0 }"
|
||||
>
|
||||
{{ field.field.name }}
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'text'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:placeholder="field.field.example"
|
||||
:autofocus="field.autofocus"
|
||||
@input="verify(key, field, false)"
|
||||
@change="verify(key, field, true)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'password'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:placeholder="field.field.example"
|
||||
:autofocus="field.autofocus"
|
||||
@input="verify(key, field, false)"
|
||||
@change="verify(key, field, true)"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-if="field.field.type === 'checkbox'"
|
||||
v-model="field.field.value"
|
||||
type="checkbox"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
@input="verify(key, field, false)"
|
||||
@change="verify(key, field, true)"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
v-if="field.field.type === 'textarea'"
|
||||
v-model="field.field.value"
|
||||
v-focus="field.autofocus"
|
||||
autocomplete="off"
|
||||
:placeholder="field.field.example"
|
||||
:name="field.field.name"
|
||||
:autofocus="field.autofocus"
|
||||
@input="verify(key, field, false)"
|
||||
@keyup="expandTextarea"
|
||||
@change="verify(key, field, true)"
|
||||
></textarea>
|
||||
|
||||
<div v-if="field.field.type === 'textdata'" class="textinfo">
|
||||
<div class="info">{{ field.field.value }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="field.field.type === 'radio'" class="items">
|
||||
<label
|
||||
v-for="(option, oKey) in field.field.example.split(',')"
|
||||
:key="oKey"
|
||||
class="field horizontal item"
|
||||
>
|
||||
<input
|
||||
v-model="field.field.value"
|
||||
type="radio"
|
||||
autocomplete="off"
|
||||
:name="field.field.name"
|
||||
:value="option"
|
||||
@input="verify(key, field, false)"
|
||||
@change="verify(key, field, true)"
|
||||
/>
|
||||
{{ option }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="field.error.length > 0" class="error">{{ field.error }}</div>
|
||||
<div v-else-if="field.message.length > 0" class="message">
|
||||
{{ field.message }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.field.description.length > 0"
|
||||
class="message"
|
||||
v-html="field.field.description"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<button
|
||||
v-if="current.submittable"
|
||||
type="submit"
|
||||
:disabled="current.submitting || disabled"
|
||||
@click="submitAndGetNext"
|
||||
>
|
||||
{{ current.actionText }}
|
||||
</button>
|
||||
<button
|
||||
v-if="current.cancellable"
|
||||
:disabled="current.submitting || disabled"
|
||||
class="secondary"
|
||||
@click="cancelAndGetNext"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./connector.css";
|
||||
import * as command from "../commands/commands.js";
|
||||
|
||||
function buildField(i, field) {
|
||||
return {
|
||||
verified: false,
|
||||
inputted: false,
|
||||
error: "",
|
||||
message: "",
|
||||
field: field,
|
||||
autofocus: i == 0
|
||||
};
|
||||
}
|
||||
|
||||
function buildEmptyCurrent() {
|
||||
return {
|
||||
data: null,
|
||||
alert: false,
|
||||
title: "",
|
||||
message: "",
|
||||
fields: [],
|
||||
actionText: "Continue",
|
||||
cancellable: false,
|
||||
submittable: false,
|
||||
submitting: false
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
focus: {
|
||||
inserted(el, binding) {
|
||||
if (!binding.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
connector: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentConnector: null,
|
||||
currentConnectorCloseWait: null,
|
||||
current: buildEmptyCurrent(),
|
||||
working: false,
|
||||
disabled: false,
|
||||
cancelled: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
async connector(oldV, newV) {
|
||||
if (this.currentConnector !== null) {
|
||||
await this.closeWizard();
|
||||
}
|
||||
|
||||
this.cancelled = false;
|
||||
this.currentConnector = newV;
|
||||
this.runWizard();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.closeWizard();
|
||||
this.runWizard();
|
||||
this.cancelled = false;
|
||||
},
|
||||
async beforeDestroy() {
|
||||
try {
|
||||
await this.closeWizard();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async sendCancel() {
|
||||
await this.closeWizard();
|
||||
|
||||
this.$emit("cancel", true);
|
||||
},
|
||||
cancel() {
|
||||
if (this.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelled = true;
|
||||
|
||||
if (this.working) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendCancel();
|
||||
},
|
||||
buildCurrent(next) {
|
||||
this.current = buildEmptyCurrent();
|
||||
|
||||
this.working = this.getConnector().wizard.started();
|
||||
|
||||
this.current.type = next.type();
|
||||
this.current.data = next.data();
|
||||
|
||||
switch (this.current.type) {
|
||||
case command.NEXT_PROMPT:
|
||||
let fields = this.current.data.inputs();
|
||||
|
||||
for (let i in fields) {
|
||||
this.current.fields.push(buildField(i, fields[i]));
|
||||
}
|
||||
|
||||
this.current.actionText = this.current.data.actionText();
|
||||
this.current.submittable = true;
|
||||
this.current.alert = true;
|
||||
this.current.cancellable = true;
|
||||
|
||||
// Fallthrough
|
||||
|
||||
case command.NEXT_WAIT:
|
||||
this.current.title = this.current.data.title();
|
||||
this.current.message = this.current.data.message();
|
||||
break;
|
||||
|
||||
case command.NEXT_DONE:
|
||||
this.working = false;
|
||||
this.disabled = true;
|
||||
|
||||
if (!this.current.data.success()) {
|
||||
this.current.title = this.current.data.error();
|
||||
this.current.message = this.current.data.message();
|
||||
} else {
|
||||
this.$emit("done", this.current.data.data());
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown command type");
|
||||
}
|
||||
|
||||
if (!this.working) {
|
||||
this.current.cancellable = false;
|
||||
}
|
||||
|
||||
return next;
|
||||
},
|
||||
getConnector() {
|
||||
if (this.currentConnector === null) {
|
||||
this.currentConnector = this.connector;
|
||||
}
|
||||
|
||||
return this.currentConnector;
|
||||
},
|
||||
async closeWizard() {
|
||||
if (this.currentConnectorCloseWait === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let waiter = this.currentConnectorCloseWait;
|
||||
|
||||
this.currentConnectorCloseWait = null;
|
||||
|
||||
this.getConnector().wizard.close();
|
||||
|
||||
await waiter;
|
||||
},
|
||||
runWizard() {
|
||||
if (this.currentConnectorCloseWait !== null) {
|
||||
throw new Error("Cannot run wizard multiple times");
|
||||
}
|
||||
|
||||
this.currentConnectorCloseWait = (async () => {
|
||||
while (!this.disabled) {
|
||||
let next = this.buildCurrent(await this.getConnector().wizard.next());
|
||||
|
||||
switch (next.type()) {
|
||||
case command.NEXT_PROMPT:
|
||||
case command.NEXT_WAIT:
|
||||
continue;
|
||||
case command.NEXT_DONE:
|
||||
return;
|
||||
|
||||
default:
|
||||
throw new Error("Unknown command type");
|
||||
}
|
||||
}
|
||||
})();
|
||||
},
|
||||
getFieldValues() {
|
||||
let mod = {};
|
||||
|
||||
for (let i in this.current.fields) {
|
||||
mod[this.current.fields[i].field.name] = this.current.fields[
|
||||
i
|
||||
].field.value;
|
||||
}
|
||||
|
||||
return mod;
|
||||
},
|
||||
expandTextarea(event) {
|
||||
event.target.style.overflowY = "hidden";
|
||||
event.target.style.height = "";
|
||||
event.target.style.height = event.target.scrollHeight + "px";
|
||||
},
|
||||
async verify(key, field, force) {
|
||||
try {
|
||||
field.message = "" + (await field.field.verify(field.field.value));
|
||||
field.inputted = true;
|
||||
field.verified = true;
|
||||
field.error = "";
|
||||
} catch (e) {
|
||||
field.error = "";
|
||||
field.message = "";
|
||||
field.verified = false;
|
||||
|
||||
if (field.inputted || force) {
|
||||
field.error = "" + e;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!field.verified &&
|
||||
(field.inputted || force) &&
|
||||
field.error.length <= 0
|
||||
) {
|
||||
field.error = "Invalid";
|
||||
}
|
||||
|
||||
this.current.fields[key] = field;
|
||||
|
||||
return field.verified;
|
||||
},
|
||||
async verifyAll() {
|
||||
let verified = true;
|
||||
|
||||
for (let i in this.current.fields) {
|
||||
if (await this.verify(i, this.current.fields[i], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
verified = false;
|
||||
}
|
||||
|
||||
return verified;
|
||||
},
|
||||
async submitAndGetNext() {
|
||||
if (this.current.submitting || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.data === null || !this.current.submittable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.verifyAll())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.submitting = true;
|
||||
|
||||
try {
|
||||
await this.current.data.submit(this.getFieldValues());
|
||||
} catch (e) {
|
||||
this.current.submitting = false;
|
||||
|
||||
alert("Submission has failed: " + e);
|
||||
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
|
||||
return;
|
||||
}
|
||||
},
|
||||
async cancelAndGetNext() {
|
||||
if (this.current.submitting || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.current.data === null || !this.current.cancellable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.submitting = true;
|
||||
|
||||
await this.current.data.cancel();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
23
ui/widgets/screen_console.css
Normal file
23
ui/widgets/screen_console.css
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#home-content > .screen > .screen-screen > .screen-console {
|
||||
}
|
||||
304
ui/widgets/screen_console.vue
Normal file
304
ui/widgets/screen_console.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="screen-console"
|
||||
:style="'background-color: ' + control.activeColor()"
|
||||
style="top: 0; right: 0; left: 0; bottom: 0; padding: 0; margin: 0; position: absolute; overflow: hidden"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Terminal } from "xterm";
|
||||
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
|
||||
import "./screen_console.css";
|
||||
import "xterm/css/xterm.css";
|
||||
import { isNumber } from "util";
|
||||
|
||||
class Term {
|
||||
constructor(control) {
|
||||
const resizeDelayInterval = 500;
|
||||
|
||||
this.term = new Terminal({
|
||||
allowTransparency: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
logLevel: process.env.NODE_ENV === "development" ? "info" : "off"
|
||||
});
|
||||
this.fit = new FitAddon();
|
||||
|
||||
this.term.loadAddon(this.fit);
|
||||
this.term.loadAddon(new WebLinksAddon());
|
||||
|
||||
this.term.setOption("theme", {
|
||||
background: control.activeColor()
|
||||
});
|
||||
|
||||
this.term.onData(data => {
|
||||
control.send(data);
|
||||
});
|
||||
|
||||
this.term.onKey(ev => {
|
||||
if (!control.echo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const printable =
|
||||
!ev.domEvent.altKey &&
|
||||
!ev.domEvent.altGraphKey &&
|
||||
!ev.domEvent.ctrlKey &&
|
||||
!ev.domEvent.metaKey;
|
||||
|
||||
if (ev.domEvent.keyCode === 13) {
|
||||
this.writeStr("\r\n");
|
||||
} else if (ev.domEvent.keyCode === 8) {
|
||||
this.writeStr("\b \b");
|
||||
} else if (printable) {
|
||||
this.writeStr(ev.key);
|
||||
}
|
||||
});
|
||||
|
||||
let resizeDelay = null,
|
||||
oldRows = 0,
|
||||
oldCols = 0;
|
||||
|
||||
this.term.onResize(dim => {
|
||||
if (dim.cols === oldCols && dim.rows === oldRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
oldRows = dim.rows;
|
||||
oldCols = dim.cols;
|
||||
|
||||
if (resizeDelay !== null) {
|
||||
clearTimeout(resizeDelay);
|
||||
resizeDelay = null;
|
||||
}
|
||||
|
||||
resizeDelay = setTimeout(() => {
|
||||
if (!isNumber(dim.cols) || !isNumber(dim.rows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dim.cols || !dim.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
control.resize({
|
||||
rows: dim.rows,
|
||||
cols: dim.cols
|
||||
});
|
||||
|
||||
resizeDelay = null;
|
||||
}, resizeDelayInterval);
|
||||
});
|
||||
}
|
||||
|
||||
init(root, callbacks) {
|
||||
this.term.open(root);
|
||||
|
||||
this.term.textarea.addEventListener("focus", callbacks.focus);
|
||||
this.term.textarea.addEventListener("blur", callbacks.blur);
|
||||
|
||||
this.term.element.addEventListener("click", () => {
|
||||
this.term.textarea.blur();
|
||||
this.term.textarea.click();
|
||||
this.term.textarea.focus();
|
||||
});
|
||||
|
||||
this.refit();
|
||||
}
|
||||
|
||||
writeStr(d) {
|
||||
try {
|
||||
this.term.write(d);
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
write(d) {
|
||||
try {
|
||||
this.term.writeUtf8(d);
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
try {
|
||||
this.term.focus();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
blur() {
|
||||
try {
|
||||
this.term.blur();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
refit() {
|
||||
try {
|
||||
this.fit.fit();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
this.term.dispose();
|
||||
} catch (e) {
|
||||
process.env.NODE_ENV === "development" && console.trace(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// So it turns out, display: none + xterm.js == trouble, so I changed this
|
||||
// to a visibility + position: absolute appoarch. Problem resolved, and I
|
||||
// like to keep it that way.
|
||||
|
||||
export default {
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
control: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
change: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
term: new Term(this.control),
|
||||
runner: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
active() {
|
||||
this.triggerActive();
|
||||
},
|
||||
change: {
|
||||
handler() {
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fit();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.deinit();
|
||||
},
|
||||
methods: {
|
||||
triggerActive() {
|
||||
this.active ? this.activate() : this.deactivate();
|
||||
},
|
||||
init() {
|
||||
let self = this;
|
||||
|
||||
this.term.init(this.$el, {
|
||||
focus(e) {
|
||||
document.addEventListener("keyup", self.localKeypress);
|
||||
document.addEventListener("keydown", self.localKeypress);
|
||||
},
|
||||
blur(e) {
|
||||
document.removeEventListener("keyup", self.localKeypress);
|
||||
document.removeEventListener("keydown", self.localKeypress);
|
||||
}
|
||||
});
|
||||
|
||||
this.triggerActive();
|
||||
this.runRunner();
|
||||
},
|
||||
async deinit() {
|
||||
await this.closeRunner();
|
||||
await this.deactivate();
|
||||
this.term.destroy();
|
||||
},
|
||||
fit() {
|
||||
this.term.refit();
|
||||
},
|
||||
localKeypress(e) {
|
||||
if (!e.altKey && !e.shiftKey && !e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
activate() {
|
||||
this.fit();
|
||||
window.addEventListener("resize", this.fit);
|
||||
this.term.focus();
|
||||
},
|
||||
async deactivate() {
|
||||
window.removeEventListener("resize", this.fit);
|
||||
document.removeEventListener("keyup", this.localKeypress);
|
||||
document.removeEventListener("keydown", this.localKeypress);
|
||||
this.term.blur();
|
||||
},
|
||||
runRunner() {
|
||||
if (this.runner !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
|
||||
this.runner = (async () => {
|
||||
try {
|
||||
for (;;) {
|
||||
this.term.write(await this.control.receive());
|
||||
|
||||
self.$emit("updated");
|
||||
}
|
||||
} catch (e) {
|
||||
self.$emit("stopped", e);
|
||||
}
|
||||
})();
|
||||
},
|
||||
async closeRunner() {
|
||||
if (this.runner === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let runner = this.runner;
|
||||
this.runner = null;
|
||||
|
||||
await runner;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
85
ui/widgets/screens.vue
Normal file
85
ui/widgets/screens.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<main style="position: relative">
|
||||
<slot v-if="screens.length <= 0"></slot>
|
||||
|
||||
<div
|
||||
v-for="(screenInfo, idx) in screens"
|
||||
v-if="screens.length > 0"
|
||||
:key="screenInfo.id"
|
||||
:style="'visibility: ' + (screen === idx ? 'visible' : 'hidden')"
|
||||
class="screen"
|
||||
style="top: 0; right: 0; left: 0; bottom: 0; padding: 0; margin: 0; overflow: auto; position: absolute;"
|
||||
>
|
||||
<div v-if="screenInfo.indicator.error.length > 0" class="screen-error">
|
||||
{{ screenInfo.indicator.error }}
|
||||
</div>
|
||||
|
||||
<div class="screen-screen" style="position: relative">
|
||||
<component
|
||||
:is="getComponent(screenInfo.control.ui())"
|
||||
:active="screen === idx"
|
||||
:control="screenInfo.control"
|
||||
:change="screenInfo.indicator"
|
||||
@stopped="stopped(idx, $event)"
|
||||
@updated="updated(idx)"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConsoleScreen from "./screen_console.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ConsoleScreen
|
||||
},
|
||||
props: {
|
||||
screen: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
screens: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getComponent(ui) {
|
||||
switch (ui) {
|
||||
case "Console":
|
||||
return "ConsoleScreen";
|
||||
|
||||
default:
|
||||
throw new Error("Unknown UI: " + ui);
|
||||
}
|
||||
},
|
||||
stopped(index, stopErr) {
|
||||
this.$emit("stopped", index, stopErr);
|
||||
},
|
||||
updated(index) {
|
||||
this.$emit("updated", index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
217
ui/widgets/status.css
Normal file
217
ui/widgets/status.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#conn-status {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
left: 96px;
|
||||
display: none;
|
||||
width: 500px;
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
#conn-status .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#conn-status:before {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#conn-status {
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#conn-status:before {
|
||||
left: 91px;
|
||||
}
|
||||
}
|
||||
|
||||
#conn-status.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#conn-status h1 {
|
||||
padding: 15px 15px 10px 15px;
|
||||
background: #a56;
|
||||
}
|
||||
|
||||
#conn-status-info {
|
||||
padding: 0 15px 15px 15px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5;
|
||||
background: #a56;
|
||||
}
|
||||
|
||||
#conn-status.green:before {
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.green h1 {
|
||||
color: #6ba;
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.green #conn-status-info {
|
||||
background: #287;
|
||||
}
|
||||
|
||||
#conn-status.yellow:before {
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.yellow h1 {
|
||||
color: #fff;
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.yellow #conn-status-info {
|
||||
background: #da0;
|
||||
}
|
||||
|
||||
#conn-status.orange:before {
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.orange h1 {
|
||||
color: #fff;
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.orange #conn-status-info {
|
||||
background: #b73;
|
||||
}
|
||||
|
||||
#conn-status.red:before {
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
#conn-status.red h1 {
|
||||
color: #fff;
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
#conn-status.red #conn-status-info {
|
||||
background: #a33;
|
||||
}
|
||||
|
||||
.conn-status-chart {
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter {
|
||||
width: 50%;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .name {
|
||||
font-size: 0.8em;
|
||||
color: #777;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .value {
|
||||
font-size: 1.5em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.conn-status-chart > .counters > .counter .value span {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart g {
|
||||
fill: none;
|
||||
stroke-width: 7px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.conn-status-chart > .chart .zero {
|
||||
stroke: #3a3a3a;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
#conn-status-delay {
|
||||
padding: 15px 0;
|
||||
background: #292929;
|
||||
}
|
||||
|
||||
#conn-status-delay > .counters > .counter {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
#conn-status-delay-chart-background {
|
||||
--color-start: #e43989;
|
||||
--color-stop: #9a5fca;
|
||||
}
|
||||
|
||||
#conn-status-delay-chart > g {
|
||||
fill: none;
|
||||
stroke: url(#conn-status-delay-chart-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-traffic {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-in-background {
|
||||
--color-start: #0287a8;
|
||||
--color-stop: #06e7b6;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-in > g {
|
||||
stroke: url(#conn-status-traffic-chart-in-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-out-background {
|
||||
--color-start: #e46226;
|
||||
--color-stop: #da356c;
|
||||
}
|
||||
|
||||
#conn-status-traffic-chart-out > g {
|
||||
stroke: url(#conn-status-traffic-chart-out-background) #2a6;
|
||||
}
|
||||
|
||||
#conn-status-close {
|
||||
/* ID mainly use for document.getElementById */
|
||||
cursor: pointer;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
}
|
||||
247
ui/widgets/status.vue
Normal file
247
ui/widgets/status.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="conn-status"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<h1 class="window-title">Connection status</h1>
|
||||
|
||||
<div id="conn-status-info">
|
||||
{{ status.description }}
|
||||
</div>
|
||||
|
||||
<div id="conn-status-delay" class="conn-status-chart">
|
||||
<div class="counters">
|
||||
<div class="counter">
|
||||
<div class="name">Delay</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.mSecondString(status.delay)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-delay-chart"
|
||||
:width="480"
|
||||
:height="50"
|
||||
type="Bar"
|
||||
:enabled="display"
|
||||
:values="status.delayHistory"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-delay-chart-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="conn-status-traffic" class="conn-status-chart">
|
||||
<div class="counters">
|
||||
<div class="counter">
|
||||
<div class="name">Inbound</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.bytePerSecondString(status.inbound)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="counter">
|
||||
<div class="name">Outbound</div>
|
||||
<div
|
||||
class="value"
|
||||
v-html="$options.filters.bytePerSecondString(status.outbound)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-traffic-chart-in"
|
||||
:width="480"
|
||||
:height="25"
|
||||
type="Bar"
|
||||
:max="inoutBoundMax"
|
||||
:enabled="display"
|
||||
:values="status.inboundHistory"
|
||||
@max="inboundMaxColUpdated"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-traffic-chart-in-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
|
||||
<div class="chart">
|
||||
<chart
|
||||
id="conn-status-traffic-chart-out"
|
||||
:width="480"
|
||||
:height="25"
|
||||
type="UpsideDownBar"
|
||||
:max="inoutBoundMax"
|
||||
:enabled="display"
|
||||
:values="status.outboundHistory"
|
||||
@max="outboundMaxColUpdated"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="conn-status-traffic-chart-out-background"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="100%"
|
||||
>
|
||||
<stop stop-color="var(--color-start)" offset="0%" />
|
||||
<stop stop-color="var(--color-stop)" offset="100%" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</chart>
|
||||
</div>
|
||||
</div>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint vue/attribute-hyphenation: 0 */
|
||||
|
||||
import "./status.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import Chart from "./chart.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
chart: Chart
|
||||
},
|
||||
filters: {
|
||||
bytePerSecondString(n) {
|
||||
const bNames = ["byte/s", "kib/s", "mib/s", "gib/s", "tib/s"];
|
||||
let remain = n,
|
||||
nUnit = bNames[0];
|
||||
|
||||
for (let i in bNames) {
|
||||
nUnit = bNames[i];
|
||||
|
||||
if (remain < 1024) {
|
||||
break;
|
||||
}
|
||||
|
||||
remain /= 1024;
|
||||
}
|
||||
|
||||
return (
|
||||
Number(remain.toFixed(2)).toLocaleString() +
|
||||
" <span>" +
|
||||
nUnit +
|
||||
"</span>"
|
||||
);
|
||||
},
|
||||
mSecondString(n) {
|
||||
const bNames = ["ms", "s", "m"];
|
||||
let remain = n,
|
||||
nUnit = bNames[0];
|
||||
|
||||
for (let i in bNames) {
|
||||
nUnit = bNames[i];
|
||||
|
||||
if (remain < 1000) {
|
||||
break;
|
||||
}
|
||||
|
||||
remain /= 1000;
|
||||
}
|
||||
|
||||
return (
|
||||
Number(remain.toFixed(2)).toLocaleString() +
|
||||
" <span>" +
|
||||
nUnit +
|
||||
"</span>"
|
||||
);
|
||||
}
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
status: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
description: "",
|
||||
delay: 0,
|
||||
delayHistory: [],
|
||||
inbound: 0,
|
||||
inboundHistory: [],
|
||||
outbound: 0,
|
||||
outboundHistory: []
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inoutBoundMax: 0,
|
||||
inboundMax: 0,
|
||||
outboundMax: 0
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
inboundMaxColUpdated(d) {
|
||||
this.inboundMax = d;
|
||||
|
||||
this.inoutBoundMax =
|
||||
this.inboundMax > this.outboundMax ? this.inboundMax : this.outboundMax;
|
||||
},
|
||||
outboundMaxColUpdated(d) {
|
||||
this.outboundMax = d;
|
||||
|
||||
this.inoutBoundMax =
|
||||
this.inboundMax > this.outboundMax ? this.inboundMax : this.outboundMax;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
103
ui/widgets/tab_list.vue
Normal file
103
ui/widgets/tab_list.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul :id="id" :class="tabsClass">
|
||||
<li
|
||||
v-for="(tabInfo, idx) in tabs"
|
||||
:key="tabInfo.id"
|
||||
:class="{
|
||||
active: tab === idx,
|
||||
error: tabInfo.indicator.error.length > 0,
|
||||
updated: tabInfo.indicator.updated
|
||||
}"
|
||||
:style="
|
||||
'background: ' +
|
||||
(tab === idx
|
||||
? tabInfo.control.activeColor()
|
||||
: tabInfo.control.color())
|
||||
"
|
||||
@click="switchTo(idx)"
|
||||
>
|
||||
<span class="title" :title="tabInfo.name">
|
||||
<span
|
||||
class="type"
|
||||
:title="tabInfo.info.name()"
|
||||
:style="'background: ' + tabInfo.info.color()"
|
||||
>
|
||||
{{ tabInfo.info.name()[0] }}
|
||||
</span>
|
||||
{{ tabInfo.name }}
|
||||
</span>
|
||||
|
||||
<span class="icon icon-close icon-close1" @click="closeAt(idx)"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tab(newVal) {
|
||||
this.switchTo(newVal);
|
||||
},
|
||||
tabs(newVal) {
|
||||
if (newVal.length > this.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.switchTo(newVal.length - 1);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTo(index) {
|
||||
if (index < 0 || index >= this.tabs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.tab == index) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("current", index);
|
||||
},
|
||||
closeAt(index) {
|
||||
this.$emit("close", index);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
148
ui/widgets/tab_window.css
Normal file
148
ui/widgets/tab_window.css
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
|
||||
#tab-window {
|
||||
z-index: 999999;
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
display: none;
|
||||
width: 400px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
#tab-window .window-frame {
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#tab-window:before {
|
||||
right: 19px;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#tab-window {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
#tab-window.display {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#tab-window-close {
|
||||
cursor: pointer;
|
||||
right: 10px;
|
||||
top: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#tab-window h1 {
|
||||
padding: 15px 15px 0 15px;
|
||||
margin-bottom: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#tab-window-list > li > .lst-wrap {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tab-window-list > li {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#tab-window-tabs {
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
opacity: 0.5;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
transition: all 0.1s linear;
|
||||
transition-property: width, top, bottom;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active::after {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.updated::after {
|
||||
background: #fff3;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.error::after {
|
||||
background: #d55;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > span.title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > span.title > span.type {
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
margin-right: 3px;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
background: #222;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li > .icon-close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
margin-top: -5px;
|
||||
color: #fff6;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#tab-window-tabs > li.active > span.title {
|
||||
padding-right: 20px;
|
||||
}
|
||||
79
ui/widgets/tab_window.vue
Normal file
79
ui/widgets/tab_window.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<window
|
||||
id="tab-window"
|
||||
flash-class="home-window-display"
|
||||
:display="display"
|
||||
@display="$emit('display', $event)"
|
||||
>
|
||||
<h1 class="window-title">Opened tabs</h1>
|
||||
|
||||
<tab-list
|
||||
id="tab-window-tabs"
|
||||
:tab="tab"
|
||||
:tabs="tabs"
|
||||
:tabs-class="tabsClass"
|
||||
@current="$emit('current', $event)"
|
||||
@close="$emit('close', $event)"
|
||||
></tab-list>
|
||||
</window>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./tab_window.css";
|
||||
|
||||
import Window from "./window.vue";
|
||||
import TabList from "./tab_list.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
window: Window,
|
||||
"tab-list": TabList
|
||||
},
|
||||
props: {
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabs(newV) {
|
||||
if (newV.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("display", false);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
76
ui/widgets/tabs.vue
Normal file
76
ui/widgets/tabs.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :id="id">
|
||||
<tab-list
|
||||
:id="id + '-tabs'"
|
||||
:tab="tab"
|
||||
:tabs="tabs"
|
||||
:tabs-class="tabsClass"
|
||||
@current="$emit('current', $event)"
|
||||
@close="$emit('close', $event)"
|
||||
></tab-list>
|
||||
|
||||
<a
|
||||
v-if="tabs.length > 0"
|
||||
:id="id + '-list'"
|
||||
:class="listTriggerClass"
|
||||
href="javascript:;"
|
||||
@click="showList"
|
||||
></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TabList from "./tab_list.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"tab-list": TabList
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
tab: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabsClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
listTriggerClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showList() {
|
||||
this.$emit("list", this.tabs);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
20
ui/widgets/window.css
Normal file
20
ui/widgets/window.css
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@charset "utf-8";
|
||||
77
ui/widgets/window.vue
Normal file
77
ui/widgets/window.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<!--
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="id"
|
||||
class="window window1"
|
||||
:class="[{ display: displaying }, { [flashClass]: displaying }]"
|
||||
>
|
||||
<div class="window-frame">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<span
|
||||
:id="id + '-close'"
|
||||
class="window-close icon icon-close1"
|
||||
@click="hide"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
display: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
flashClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
displaying: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
display(newVal) {
|
||||
newVal ? this.show() : this.hide();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.displaying = true;
|
||||
|
||||
this.$emit("display", this.displaying);
|
||||
},
|
||||
hide() {
|
||||
this.displaying = false;
|
||||
|
||||
this.$emit("display", this.displaying);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user