Skip to content

Commit cdffb16

Browse files
refactor(ui): improve terminal connection UI and add SSHID helper
Enhance the terminal connection user experience with: - Updated dialog title from 'Terminal Login' to 'Connect to Device' - Added inline SSHID usage hint with 'Show me how' button - Refactored TerminalHelper component
1 parent c076f28 commit cdffb16

File tree

14 files changed

+305
-296
lines changed

14 files changed

+305
-296
lines changed

ui/src/components/QuickConnection/QuickConnectionList.vue

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,13 @@
6464
<CopyWarning
6565
ref="copyRef"
6666
:copied-item="'Device SSHID'"
67-
:bypass="shouldOpenTerminalHelper()"
6867
:macro="getSshid(item)"
6968
>
7069
<template #default="{ copyText }">
7170
<span
7271
v-bind="props"
7372
tabindex="0"
74-
class="hover-text"
73+
class="hover-text text-mono"
7574
data-test="copy-id-button"
7675
@click.stop="handleSshidClick(item, copyText)"
7776
@keypress.enter.stop="handleSshidClick(item, copyText)"
@@ -81,8 +80,25 @@
8180
</template>
8281
</CopyWarning>
8382
</template>
84-
<span>Copy ID</span>
83+
<span>Copy SSHID</span>
8584
</v-tooltip>
85+
86+
<template #append>
87+
<v-tooltip location="bottom">
88+
<template #activator="{ props }">
89+
<v-icon
90+
v-bind="props"
91+
icon="mdi-help-circle-outline"
92+
size="small"
93+
color="primary"
94+
class="ml-2"
95+
data-test="sshid-help-btn"
96+
@click.stop="forceOpenTerminalHelper(item)"
97+
/>
98+
</template>
99+
<span>What is an SSHID?</span>
100+
</v-tooltip>
101+
</template>
86102
</v-chip>
87103
</v-col>
88104
<v-col
@@ -125,12 +141,10 @@
125141
</v-row>
126142
</v-list-item>
127143
</v-list>
128-
<TerminalHelper
144+
<SSHIDHelper
129145
v-if="showTerminalHelper"
130146
v-model="showTerminalHelper"
131147
:sshid="selectedSshid"
132-
:user-id="userId"
133-
:show-checkbox="true"
134148
/>
135149
<TerminalDialog
136150
v-model="showDialog"
@@ -145,7 +159,7 @@ import { ref, onMounted, computed, watch } from "vue";
145159
import { VList } from "vuetify/components";
146160
import { useMagicKeys } from "@vueuse/core";
147161
import TerminalDialog from "../Terminal/TerminalDialog.vue";
148-
import TerminalHelper from "../Terminal/TerminalHelper.vue";
162+
import SSHIDHelper from "../Terminal/SSHIDHelper.vue";
149163
import CopyWarning from "@/components/User/CopyWarning.vue";
150164
import { displayOnlyTenCharacters } from "@/utils/string";
151165
import showTag from "@/utils/tag";
@@ -168,7 +182,6 @@ const selectedDeviceName = ref("");
168182
const showDialog = ref(false);
169183
const showTerminalHelper = ref(false);
170184
const selectedSshid = ref("");
171-
const userId = authStore.id;
172185
const onlineDevices = computed(() => devicesStore.onlineDevices);
173186
174187
const filter = computed(() =>
@@ -213,25 +226,14 @@ const openTerminalHelper = (item: IDevice) => {
213226
showTerminalHelper.value = true;
214227
};
215228
216-
const shouldOpenTerminalHelper = () => {
217-
try {
218-
const dispensedUsers = JSON.parse(
219-
localStorage.getItem("dispenseTerminalHelper") || "[]",
220-
) as string[];
221-
return !dispensedUsers.includes(userId);
222-
} catch {
223-
return true;
224-
}
225-
};
226-
227229
const handleSshidClick = (item: IDevice, copyFn: (text: string) => void) => {
228-
if (shouldOpenTerminalHelper()) {
229-
openTerminalHelper(item);
230-
return;
231-
}
232230
copyFn(getSshid(item));
233231
};
234232
233+
const forceOpenTerminalHelper = (item: IDevice) => {
234+
openTerminalHelper(item);
235+
};
236+
235237
const openTerminalMacro = (value: IDevice) => {
236238
let executed = false;
237239

ui/src/components/Tables/DeviceTable.vue

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,39 @@
4040
<span>{{ item.info.pretty_name }}</span>
4141
</td>
4242
<td class="text-center">
43-
<CopyWarning
44-
:copied-item="'Device SSHID'"
45-
:bypass="shouldOpenTerminalHelper()"
46-
>
43+
<CopyWarning :copied-item="'Device SSHID'">
4744
<template #default="{ copyText }">
4845
<v-chip data-test="sshid-chip">
4946
<v-tooltip location="bottom">
5047
<template #activator="{ props }">
5148
<span
5249
v-bind="props"
53-
class="hover-text"
50+
class="hover-text text-mono"
5451
@click="handleSshidClick(item, copyText)"
5552
@keypress.enter="handleSshidClick(item, copyText)"
5653
>
5754
{{ getSshid(item) }}
5855
</span>
5956
</template>
60-
<span>{{
61-
shouldOpenTerminalHelper()
62-
? "Show connection instructions"
63-
: "Copy ID"
64-
}}</span>
57+
<span>Copy SSHID</span>
6558
</v-tooltip>
59+
60+
<template #append>
61+
<v-tooltip location="bottom">
62+
<template #activator="{ props }">
63+
<v-icon
64+
v-bind="props"
65+
icon="mdi-help-circle-outline"
66+
size="small"
67+
color="primary"
68+
class="ml-2"
69+
data-test="sshid-help-btn"
70+
@click.stop="forceOpenTerminalHelper(item)"
71+
/>
72+
</template>
73+
<span>What is an SSHID?</span>
74+
</v-tooltip>
75+
</template>
6676
</v-chip>
6777
</template>
6878
</CopyWarning>
@@ -251,13 +261,11 @@
251261
</tr>
252262
</template>
253263
</DataTable>
254-
<TerminalHelper
264+
<SSHIDHelper
255265
v-if="showTerminalHelper"
256266
v-model="showTerminalHelper"
257267
:sshid="selectedSshid"
258-
:user-id="userId"
259-
:show-checkbox="true"
260-
data-test="terminal-helper-component"
268+
data-test="sshid-helper-component"
261269
/>
262270
</div>
263271
</template>
@@ -272,7 +280,7 @@ import DeviceDelete from "../Devices/DeviceDelete.vue";
272280
import TagFormUpdate from "../Tags/TagFormUpdate.vue";
273281
import TerminalConnectButton from "../Terminal/TerminalConnectButton.vue";
274282
import CopyWarning from "@/components/User/CopyWarning.vue";
275-
import TerminalHelper from "../Terminal/TerminalHelper.vue";
283+
import SSHIDHelper from "../Terminal/SSHIDHelper.vue";
276284
import { IDevice, IDeviceMethods, DeviceStatus } from "@/interfaces/IDevice";
277285
import hasPermission from "@/utils/permission";
278286
import showTag from "@/utils/tag";
@@ -304,7 +312,6 @@ const sortField = ref<string>();
304312
const sortOrder = ref<"asc" | "desc">();
305313
const showTerminalHelper = ref(false);
306314
const selectedSshid = ref("");
307-
const userId = authStore.id;
308315
309316
const headers = [
310317
{
@@ -390,25 +397,14 @@ const openTerminalHelper = (item: IDevice) => {
390397
showTerminalHelper.value = true;
391398
};
392399
393-
const shouldOpenTerminalHelper = () => {
394-
try {
395-
const dispensedUsers = JSON.parse(
396-
localStorage.getItem("dispenseTerminalHelper") || "[]",
397-
) as string[];
398-
return !dispensedUsers.includes(userId);
399-
} catch {
400-
return true;
401-
}
402-
};
403-
404400
const handleSshidClick = (item: IDevice, copyFn: (text: string) => void) => {
405-
if (shouldOpenTerminalHelper()) {
406-
openTerminalHelper(item);
407-
return;
408-
}
409401
copyFn(getSshid(item));
410402
};
411403
404+
const forceOpenTerminalHelper = (item: IDevice) => {
405+
openTerminalHelper(item);
406+
};
407+
412408
const canUpdateDeviceTag = hasPermission("tag:update");
413409
414410
const canRemoveDevice = hasPermission("device:remove");
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<template>
2+
<WindowDialog
3+
v-model="showDialog"
4+
transition="dialog-bottom-transition"
5+
title="What is an SSHID?"
6+
icon="mdi-ssh"
7+
@close="close"
8+
>
9+
<v-card-text class="pa-6">
10+
<p class="text-body-2 mb-4">
11+
The SSHID is a unique identifier that allows you to connect to your device from anywhere.
12+
Use it in scripts, CI/CD pipelines, automations, or your local terminal.
13+
It works exactly like traditional SSH.
14+
</p>
15+
16+
<v-divider class="mb-4" />
17+
18+
<div class="text-subtitle-2 font-weight-bold mb-3">
19+
Examples
20+
</div>
21+
22+
<v-expansion-panels
23+
variant="accordion"
24+
elevation="0"
25+
class="bg-v-theme-surface"
26+
>
27+
<v-expansion-panel
28+
v-for="(example, index) in examples"
29+
:key="index"
30+
class="bg-v-theme-surface"
31+
>
32+
<v-expansion-panel-title>
33+
<div class="d-flex align-center w-100">
34+
<v-icon
35+
:icon="example.icon"
36+
size="large"
37+
class="mr-3"
38+
color="primary"
39+
/>
40+
<div class="flex-grow-1">
41+
<div class="text-subtitle-1 font-weight-medium">
42+
{{ example.title }}
43+
</div>
44+
<div class="text-body-2 text-medium-emphasis">
45+
{{ example.description }}
46+
</div>
47+
</div>
48+
</div>
49+
</v-expansion-panel-title>
50+
<v-expansion-panel-text>
51+
<div class="pa-4">
52+
<CopyCommandField
53+
:command="example.command"
54+
:hide-details="true"
55+
density="compact"
56+
/>
57+
</div>
58+
</v-expansion-panel-text>
59+
</v-expansion-panel>
60+
</v-expansion-panels>
61+
</v-card-text>
62+
63+
<template #footer>
64+
<v-card-actions class="d-flex justify-end w-100">
65+
<v-btn
66+
data-test="close-btn"
67+
@click="close"
68+
>
69+
Close
70+
</v-btn>
71+
</v-card-actions>
72+
</template>
73+
</WindowDialog>
74+
</template>
75+
76+
<script setup lang="ts">
77+
import { computed } from "vue";
78+
import WindowDialog from "@/components/Dialogs/WindowDialog.vue";
79+
import CopyCommandField from "@/components/CopyCommandField.vue";
80+
81+
interface Props {
82+
sshid: string;
83+
}
84+
85+
const props = defineProps<Props>();
86+
87+
const showDialog = defineModel<boolean>({ required: true });
88+
89+
const username = "<username>";
90+
91+
const examples = computed(() => [
92+
{
93+
title: "Interactive SSH Session",
94+
description: "Connect to your device and get an interactive shell",
95+
icon: "mdi-console",
96+
command: `ssh ${username}@${props.sshid}`,
97+
},
98+
{
99+
title: "Execute Remote Command",
100+
description: "Run a command on the device and see the output",
101+
icon: "mdi-play-circle-outline",
102+
command: `ssh ${username}@${props.sshid} "ls -la"`,
103+
},
104+
{
105+
title: "Upload File (SCP)",
106+
description: "Copy a file from your local machine to the device",
107+
icon: "mdi-upload",
108+
command: `scp file.txt ${username}@${props.sshid}:/path/to/destination/`,
109+
},
110+
{
111+
title: "Download File (SCP)",
112+
description: "Copy a file from the device to your local machine",
113+
icon: "mdi-download",
114+
command: `scp ${username}@${props.sshid}:/path/to/file.txt ./`,
115+
},
116+
{
117+
title: "Port Forwarding",
118+
description: "Forward a local port to a port on the device (e.g., access device's web server)",
119+
icon: "mdi-lan-connect",
120+
command: `ssh -L 8080:localhost:80 ${username}@${props.sshid}`,
121+
},
122+
]);
123+
124+
const close = () => {
125+
showDialog.value = false;
126+
};
127+
128+
defineExpose({ showDialog });
129+
</script>

ui/src/components/Terminal/TerminalConnectButton.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
v-model="showWebTerminal"
4949
:device-uid
5050
:device-name
51+
:sshid
5152
/>
52-
<TerminalHelper
53+
<SSHIDHelper
5354
v-model="showTerminalHelper"
5455
:sshid
5556
/>
@@ -58,7 +59,7 @@
5859
<script setup lang="ts">
5960
import { reactive, ref } from "vue";
6061
import TerminalDialog from "./TerminalDialog.vue";
61-
import TerminalHelper from "./TerminalHelper.vue";
62+
import SSHIDHelper from "./SSHIDHelper.vue";
6263
6364
defineOptions({
6465
inheritAttrs: false,

ui/src/components/Terminal/TerminalDialog.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
v-if="showLoginForm"
44
v-model="showLoginDialog"
55
v-model:loading="isConnecting"
6+
:sshid="sshid"
67
@submit="handleSubmit"
78
@close="close"
89
/>
@@ -36,9 +37,10 @@ import Terminal from "./Terminal.vue";
3637
// Utility to create key fingerprint for private key auth
3738
import { convertToFingerprint } from "@/utils/sshKeys";
3839
39-
const { deviceUid, deviceName } = defineProps<{
40+
const { deviceUid, deviceName, sshid } = defineProps<{
4041
deviceUid: string;
4142
deviceName: string;
43+
sshid?: string;
4244
}>();
4345
4446
const route = useRoute(); // current route

0 commit comments

Comments
 (0)