Initial commit

This commit is contained in:
NI
2019-08-07 15:56:51 +08:00
commit 02f14eb14f
206 changed files with 38863 additions and 0 deletions

5
ui/widgets/busy.svg Normal file
View 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
View 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
View 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
View 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>

View 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;
}

View 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>

View 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;
}

View 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>

View 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;
}

View 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
View 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
View 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
View 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>

View 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 {
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>