mirror of
https://github.com/beekeeper-studio/beekeeper-studio.git
synced 2026-03-13 10:12:54 +08:00
add ssh jump hosts table component
This commit is contained in:
@@ -44,7 +44,13 @@ import Vue from 'vue'
|
||||
expanded() {
|
||||
this.toggleContent = this.expanded
|
||||
this.$emit('expanded', this.expanded)
|
||||
}
|
||||
},
|
||||
toggleContent: {
|
||||
handler() {
|
||||
this.$emit('toggleContent', this.toggleContent)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
toggleIcon() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<toggle-form-area
|
||||
title="SSH Tunnel"
|
||||
hide-toggle="true"
|
||||
:expanded="config.sshEnabled"
|
||||
@toggleContent="toggleContent = $event"
|
||||
:expanded="true"
|
||||
:initially-expanded="true"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<x-switch
|
||||
@@ -17,6 +18,9 @@
|
||||
<div>For the SSH tunnel to work, AllowTcpForwarding must be set to "yes" in your ssh server config.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ssh-jump-hosts-table v-if="toggleContent" :rows="rows" />
|
||||
|
||||
<div class="row gutter">
|
||||
<div class="col s9 form-group">
|
||||
<label for="sshHost">SSH Hostname</label>
|
||||
@@ -35,24 +39,8 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gutter">
|
||||
<div class="col s6 form-group">
|
||||
<label for="bastionHost">Bastion Host (Jump Host)</label>
|
||||
<masked-input
|
||||
:value="config.sshBastionHost"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshBastionHost = val"
|
||||
/>
|
||||
</div>
|
||||
<div class="col s3 form-group">
|
||||
<label for="bastionHost">Bastion Port</label>
|
||||
<masked-input
|
||||
:value="config.sshBastionPort"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshBastionPort = val"
|
||||
/>
|
||||
</div>
|
||||
<div class="col s3 form-group">
|
||||
<div class="row">
|
||||
<div class="col form-group">
|
||||
<label for="sshKeepaliveInterval">
|
||||
Keepalive Interval <i
|
||||
class="material-icons"
|
||||
@@ -81,71 +69,56 @@
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="config.sshMode === 'agent'"
|
||||
class="agent flex-col"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label for="sshUsername">SSH Username</label>
|
||||
<masked-input
|
||||
:value="config.sshUsername"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshUsername = val"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="alert alert-warning"
|
||||
v-if="$config.isSnap"
|
||||
>
|
||||
<i class="material-icons">error_outline</i>
|
||||
<div>
|
||||
SSH Agent Forwarding is not possible with the Snap version of Beekeeper Studio due to the security model of Snap apps.
|
||||
<external-link :href="enableSshLink">
|
||||
Read more
|
||||
</external-link>
|
||||
<div class="ssh-agent-indicator" v-if="config.sshMode === 'agent'">
|
||||
<div
|
||||
class="error"
|
||||
v-if="$config.isSnap"
|
||||
>
|
||||
<i class="material-icons">error_outline</i>
|
||||
<div>
|
||||
SSH Agent Forwarding is not possible with the Snap version of Beekeeper Studio due to the security model of Snap apps.
|
||||
<external-link :href="enableSshLink">
|
||||
Read more
|
||||
</external-link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$config.sshAuthSock"
|
||||
class="success"
|
||||
>
|
||||
<i class="material-icons">check</i>
|
||||
<div>We found your SSH Agent. You're good to go!</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$config.isWindows"
|
||||
class="info"
|
||||
>
|
||||
<i class="material-icons-outlined">info</i>
|
||||
<div>We didn't find a *nix ssh-agent running, so we'll attempt to use the PuTTY agent, pageant.</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="warning"
|
||||
>
|
||||
<i class="material-icons">error_outline</i>
|
||||
<div>You don't seem to have an SSH agent running.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$config.sshAuthSock"
|
||||
class="alert alert-success"
|
||||
>
|
||||
<i class="material-icons">check</i>
|
||||
<div>We found your SSH Agent. You're good to go!</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="$config.isWindows"
|
||||
class="alert alert-info"
|
||||
>
|
||||
<i class="material-icons-outlined">info</i>
|
||||
<div>We didn't find a *nix ssh-agent running, so we'll attempt to use the PuTTY agent, pageant.</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="alert alert-warning"
|
||||
>
|
||||
<i class="material-icons">error_outline</i>
|
||||
<div>You don't seem to have an SSH agent running.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sshUsername">SSH Username</label>
|
||||
<masked-input
|
||||
:value="config.sshUsername"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshUsername = val"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="config.sshMode === 'keyfile'"
|
||||
class="private-key gutter"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-group">
|
||||
<label for="sshUsername">SSH Username</label>
|
||||
<masked-input
|
||||
:value="config.sshUsername"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshUsername = val"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$config.isSnap && !$config.snapSshPlug"
|
||||
class="row"
|
||||
@@ -159,51 +132,55 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gutter">
|
||||
<div class="col s6 form-group">
|
||||
<label for="sshKeyfile">Private Key File</label>
|
||||
<file-picker
|
||||
v-model="config.sshKeyfile"
|
||||
editable
|
||||
:show-hidden-files="true"
|
||||
:default-path="filePickerDefaultPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="col s6 form-group">
|
||||
<label for="sshKeyfilePassword">Key File PassPhrase <span class="hint">(Optional)</span></label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
v-model="config.sshKeyfilePassword"
|
||||
>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label for="sshKeyfile">Private Key File</label>
|
||||
<file-picker
|
||||
v-model="config.sshKeyfile"
|
||||
editable
|
||||
:show-hidden-files="true"
|
||||
:default-path="filePickerDefaultPath"
|
||||
/>
|
||||
</div>
|
||||
<div class="row form-group">
|
||||
<label for="sshKeyfilePassword">Key File PassPhrase <span class="hint">(Optional)</span></label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
v-model="config.sshKeyfilePassword"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="config.sshMode === 'userpass'"
|
||||
class="row gutter"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="col s6">
|
||||
<div class="form-group">
|
||||
<label for="sshUsername">SSH Username</label>
|
||||
<masked-input
|
||||
:value="config.sshUsername"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshUsername = val"
|
||||
/>
|
||||
</div>
|
||||
<label for="sshPassword">SSH Password</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="password"
|
||||
v-model="config.sshPassword"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="row gutter" v-if="false">
|
||||
<div class="col s9 form-group">
|
||||
<label for="bastionHost">Bastion Host (Jump Host)</label>
|
||||
<masked-input
|
||||
:value="config.sshBastionHost"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshBastionHost = val"
|
||||
/>
|
||||
</div>
|
||||
<div class="col s6">
|
||||
<div class="form-group">
|
||||
<label for="sshPassword">SSH Password</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="password"
|
||||
v-model="config.sshPassword"
|
||||
>
|
||||
</div>
|
||||
<div class="col s3 form-group">
|
||||
<label for="bastionHost">Port</label>
|
||||
<masked-input
|
||||
:value="config.sshBastionPort"
|
||||
:privacyMode="privacyMode"
|
||||
@input="val => config.sshBastionPort = val"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</toggle-form-area>
|
||||
</template>
|
||||
@@ -213,12 +190,14 @@ import ExternalLink from '@/components/common/ExternalLink.vue'
|
||||
import ToggleFormArea from '../common/ToggleFormArea.vue'
|
||||
import MaskedInput from '@/components/MaskedInput.vue'
|
||||
import { mapState } from 'vuex'
|
||||
import SshJumpHostsTable from '@/components/connection/SshJumpHostsTable.vue'
|
||||
|
||||
export default {
|
||||
props: ['config'],
|
||||
components: {
|
||||
FilePicker, ExternalLink,
|
||||
ToggleFormArea, MaskedInput
|
||||
ToggleFormArea, MaskedInput,
|
||||
SshJumpHostsTable,
|
||||
},
|
||||
computed: {
|
||||
...mapState('settings', ['privacyMode']),
|
||||
@@ -231,9 +210,23 @@ export default {
|
||||
{ label: "Username & Password", mode: "userpass" },
|
||||
{ label: "SSH Agent", mode: "agent" }
|
||||
],
|
||||
filePickerDefaultPath: window.main.join(platformInfo.homeDirectory, '.ssh')
|
||||
filePickerDefaultPath: window.main.join(platformInfo.homeDirectory, '.ssh'),
|
||||
toggleContent: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rows() {
|
||||
return [
|
||||
{
|
||||
host: `${this.config.sshHost ?? ''}:${this.config.sshPort ?? ''}`,
|
||||
username: this.config.sshUsername ?? '',
|
||||
auth: this.config.sshMode ?? '',
|
||||
},
|
||||
{ host: "localhost:3601", username: "azmy60", auth: "Password" },
|
||||
{ host: "localhost:3601", username: "azmy60", auth: "Key File" },
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setMode(option) {
|
||||
this.config.sshMode = option.mode
|
||||
@@ -241,3 +234,30 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ssh-agent-indicator > div {
|
||||
display: flex;
|
||||
font-size: 0.76em;
|
||||
gap: 0.4em;
|
||||
padding-top: 0.5em;
|
||||
|
||||
.material-icons, .material-icons-outlined {
|
||||
font-size: 1.17em;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--brand-success);
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: var(--brand-info);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: var(--brand-warning);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
296
apps/studio/src/components/connection/SshJumpHostsTable.vue
Normal file
296
apps/studio/src/components/connection/SshJumpHostsTable.vue
Normal file
@@ -0,0 +1,296 @@
|
||||
<template>
|
||||
<div class="ssh-table-container">
|
||||
<div class="ssh-orders">
|
||||
<div v-for="(_row, idx) in rows" :key="idx" class="host-bullet" :title="idx === rows.length - 1 ? 'Target Host' : `Jump Host #${idx + 1}`
|
||||
" />
|
||||
<div class="add-host-bullet">
|
||||
<i class="material-icons">add_circle</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ssh-table-rows">
|
||||
<div class="ssh-table" ref="sshTable" />
|
||||
<button type="button" class="btn btn-flat add-host-btn">
|
||||
Add Jump Host
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { Tabulator } from "tabulator-tables";
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
rows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
sshTable: null as Tabulator | null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
async rows() {
|
||||
if (!this.sshTable) {
|
||||
return;
|
||||
}
|
||||
const selectedRowPosition =
|
||||
this.sshTable.getSelectedRows()[0]?.getPosition() ?? 1;
|
||||
await this.sshTable.setData(this.rows);
|
||||
this.sshTable.selectRow(
|
||||
this.sshTable.getRowFromPosition(selectedRowPosition)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
createTable() {
|
||||
this.destroyTable();
|
||||
|
||||
const sshTable = new Tabulator(this.$refs.sshTable, {
|
||||
layout: "fitColumns",
|
||||
movableRows: true,
|
||||
rowHeader: {
|
||||
headerSort: false,
|
||||
resizable: false,
|
||||
minWidth: 30,
|
||||
width: 30,
|
||||
rowHandle: true,
|
||||
formatter: "handle",
|
||||
},
|
||||
columnDefaults: {
|
||||
headerSort: false,
|
||||
resizable: false,
|
||||
},
|
||||
columns: [
|
||||
{ title: "Host", field: "host", minWidth: 140 },
|
||||
{ title: "Username", field: "username", minWidth: 72 },
|
||||
{
|
||||
title: "Auth",
|
||||
field: "auth",
|
||||
minWidth: 70,
|
||||
formatter: "lookup",
|
||||
formatterParams: {
|
||||
agent: "Agent",
|
||||
userpass: "Password",
|
||||
keyfile: "Key File",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
cssClass: "action",
|
||||
formatter(cell) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.classList.add("btn", "btn-fab");
|
||||
const icon = document.createElement("i");
|
||||
icon.classList.add("material-icons");
|
||||
icon.innerText = "clear";
|
||||
button.appendChild(icon);
|
||||
button.onclick = () => {
|
||||
console.log("hello ", cell.getRow().getData());
|
||||
};
|
||||
return button;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
this.sshTable = sshTable;
|
||||
|
||||
sshTable.on("tableBuilt", async () => {
|
||||
await sshTable.setData(this.rows);
|
||||
sshTable.getRowFromPosition(1).select();
|
||||
});
|
||||
sshTable.on("rowClick", (_e, row) => {
|
||||
if (row.isSelected()) {
|
||||
return;
|
||||
}
|
||||
row
|
||||
.getTable()
|
||||
.getSelectedRows()
|
||||
.forEach((r) => r.deselect());
|
||||
row.select();
|
||||
});
|
||||
},
|
||||
destroyTable() {
|
||||
if (this.sshTable) {
|
||||
this.sshTable.destroy();
|
||||
this.sshTable = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.createTable();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyTable();
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ssh-table-container {
|
||||
display: flex;
|
||||
|
||||
.ssh-table-rows {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ssh-orders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// width: 1.5rem;
|
||||
padding-top: calc(2.28rem + 3px);
|
||||
gap: 3px;
|
||||
margin-right: 0.75rem;
|
||||
color: var(--text-lighter);
|
||||
|
||||
>* {
|
||||
height: 2.14rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&.host-bullet::before {
|
||||
content: "";
|
||||
width: 0.75em;
|
||||
height: 0.75em;
|
||||
background-color: currentColor;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
&.add-host-bullet {
|
||||
position: relative;
|
||||
|
||||
.material-icons {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
margin-inline: auto;
|
||||
bottom: -0.65rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: "Material Icons";
|
||||
font-size: 1em;
|
||||
content: "more_vert";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn.add-host-btn {
|
||||
width: 100%;
|
||||
padding-left: 2.5rem;
|
||||
color: var(--text-dark);
|
||||
font-size: 0.831rem;
|
||||
font-weight: normal;
|
||||
justify-content: flex-start;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
background-color: rgb(from var(--theme-base) r g b / 4%);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.ssh-table ::v-deep {
|
||||
&.tabulator {
|
||||
.tabulator-header {
|
||||
box-shadow: none;
|
||||
|
||||
.tabulator-col {
|
||||
.tabulator-col-content {
|
||||
padding-inline: 0.4rem;
|
||||
}
|
||||
|
||||
.tabulator-col-title {
|
||||
color: var(--text-lighter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
background-color: rgb(from var(--theme-base) r g b / 5%);
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-row {
|
||||
margin: 3px 0;
|
||||
color: var(--text-dark);
|
||||
border-radius: 5px;
|
||||
font-size: 0.831rem;
|
||||
|
||||
&.tabulator-selected {
|
||||
outline-offset: -1px;
|
||||
outline: 1px solid var(--input-highlight);
|
||||
}
|
||||
|
||||
.tabulator-row-header {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.tabulator-cell {
|
||||
|
||||
&,
|
||||
&.tabulator-row-header {
|
||||
padding-inline: 0.4rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&.action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0;
|
||||
|
||||
button {
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover .material-icons {
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
color: var(--text-lighter);
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabulator-row-handle-box {
|
||||
width: auto;
|
||||
display: flex;
|
||||
height: 60%;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tabulator-row-handle-bar {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 1px;
|
||||
background-color: var(--border-color);
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -152,8 +152,9 @@ class SSHConnection {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
const connection = await this.connect(this.options.endHost, undefined, stream)
|
||||
return resolve(connection)
|
||||
this.connect(this.options.endHost, this.options.endPort, stream)
|
||||
.catch(reject)
|
||||
.then(resolve);
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -165,7 +166,7 @@ class SSHConnection {
|
||||
|
||||
const options = {
|
||||
host,
|
||||
port,
|
||||
port: port || 22,
|
||||
username: this.options.username,
|
||||
password: this.options.password,
|
||||
privateKey: this.options.privateKey,
|
||||
|
||||
Reference in New Issue
Block a user