Skip to content

Enhance SSH dialog user experience #4260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e3f4a2c
feat[SSH Key dialog]:
0oM4R Jun 29, 2025
3790309
Chore: make handlepostlogin after setting the profile
0oM4R Jun 29, 2025
fa2c46b
handle multiple sshkeys in the sshmanager
0oM4R Jun 29, 2025
6d3de06
feat: add validator to allow alphanumiric with inner space
0oM4R Jun 29, 2025
2bac846
Feat: support update sshkes names
0oM4R Jun 29, 2025
c4f2620
fix: update SSH key management logic to use oldKeys for default name …
0oM4R Jun 30, 2025
3bc8b86
fix: update loading state and message for SSH key recovery
0oM4R Jun 30, 2025
f4224a6
fix: update isAlphanumericWithSpace validator return type and logic
0oM4R Jun 30, 2025
bc9c578
feat: enhance SSH key name validation and trim trailing spaces
0oM4R Jul 14, 2025
6c813b7
feat: add name validation for SSH keys with alphanumeric and length c…
0oM4R Jul 14, 2025
4bac672
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Jul 14, 2025
aafda9b
feat: add success toast notification when SSH keys are recovered
0oM4R Jul 14, 2025
856b62d
eslint: fix workflow
0oM4R Jul 14, 2025
a367275
test: allow single trailing space in alphanumeric validator
0oM4R Jul 14, 2025
dc045c0
resote doc file
0oM4R Jul 15, 2025
a100173
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Jul 15, 2025
9c789b8
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Jul 20, 2025
a45cc95
feat: add form validation to SSH key dialog with disabled save button
0oM4R Jul 20, 2025
996fffd
refactor: improve SSH key name validation and uniqueness handling
0oM4R Jul 20, 2025
bda0b06
test: split alphanumeric validation tests into more focused test cases
0oM4R Jul 20, 2025
ce80314
Merge branch 'development' of github.com:threefoldtech/tfgrid-sdk-ts …
0oM4R Jul 22, 2025
a68d30c
fix: disable save button when form is invalid and remove unused compu…
0oM4R Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion packages/grid_client/docs/api/classes/nodes.html

Large diffs are not rendered by default.

89 changes: 16 additions & 73 deletions packages/playground/src/components/node_details.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<v-dialog
v-model="$props.openDialog"
v-if="$props.openDialog"
v-model="$props.openDialog"
transition="dialog-bottom-transition"
hide-overlay
attach="#modals"
Expand All @@ -11,14 +11,8 @@
>
<v-toolbar color="primary">
<div class="d-flex justify-center">
<v-btn
icon
dark
@click="() => $emit('close-dialog', false)"
>
<v-icon color="anchor">
mdi-close
</v-icon>
<v-btn icon dark @click="() => $emit('close-dialog', false)">
<v-icon color="anchor"> mdi-close </v-icon>
</v-btn>
</div>
</v-toolbar>
Expand All @@ -33,87 +27,37 @@
<template v-else-if="isError">
<v-card class="d-flex justify-center align-center h-screen">
<div class="text-center w-100 pa-3">
<v-icon
variant="tonal"
color="error"
style="font-size: 50px"
icon="mdi-close-circle-outline"
/>
<v-icon variant="tonal" color="error" style="font-size: 50px" icon="mdi-close-circle-outline" />
<p class="mt-4 mb-4 font-weight-bold text-error">
{{ errorMessage }}
</p>
<v-btn
class="mr-4"
text="Try Again"
@click="requestNode"
/>
<v-btn
color="error"
text="Cancel"
@click="(val: boolean) => closeDialog(val)"
/>
<v-btn class="mr-4" text="Try Again" @click="requestNode" />
<v-btn color="error" text="Cancel" @click="(val: boolean) => closeDialog(val)" />
</div>
</v-card>
</template>

<template v-else>
<v-card>
<node-resources-charts
:node="node"
:is-live-stats="isLiveStats"
:hint-message="errorLoadingStatsMessage"
/>
<v-row
class="pa-8 mt-5"
justify-md="start"
justify-sm="center"
>
<v-col
cols="12"
md="6"
sm="12"
>
<node-resources-charts :node="node" :is-live-stats="isLiveStats" :hint-message="errorLoadingStatsMessage" />
<v-row class="pa-8 mt-5" justify-md="start" justify-sm="center">
<v-col cols="12" md="6" sm="12">
<node-details-card :node="node" />
<farm-details-card
class="mt-5"
:node="node"
/>
<interfaces-details-card
class="mt-5"
:node="node"
/>
<farm-details-card class="mt-5" :node="node" />
<interfaces-details-card class="mt-5" :node="node" />
<public-config-details-card
v-if="node.publicConfig && node.publicConfig.domain"
class="mt-5"
:node="node"
/>

<cpu-benchmark-card
v-if="hasActiveProfile && node.healthy"
class="mt-5"
:node="node"
/>
<cpu-benchmark-card v-if="hasActiveProfile && node.healthy" class="mt-5" :node="node" />
</v-col>
<v-col
cols="12"
md="6"
sm="12"
>
<v-col cols="12" md="6" sm="12">
<country-details-card :node="node" />
<twin-details-card
class="mt-3"
:node="node"
/>
<gpu-details-card
v-if="node.gpus?.length"
class="mt-3"
:node="node"
/>
<i-perf-card
v-if="hasActiveProfile && node.healthy"
class="mt-3"
:node="node"
/>
<twin-details-card class="mt-3" :node="node" />
<gpu-details-card v-if="node.gpus?.length" class="mt-3" :node="node" />
<i-perf-card v-if="hasActiveProfile && node.healthy" class="mt-3" :node="node" />
</v-col>
</v-row>
</v-card>
Expand Down Expand Up @@ -143,7 +87,6 @@ import { getNode, getNodeStatusColor } from "@/utils/get_nodes";
import IPerfCard from "./node_details_cards/iperf_details_card.vue";
import NodeResourcesCharts from "./node_resources_charts.vue";
export default {

components: {
NodeResourcesCharts,
NodeDetailsCard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,12 @@ async function storeAndLogin() {
const grid = await getGrid({ mnemonic: mnemonic.value, keypairType: keypairType.value });
storeEmail(grid!, email.value);
setCredentials(md5(password.value), mnemonicHash, keypairTypeHash, md5(email.value));
await handlePostLogin(grid!, password.value, email.value);
const profile = await loadProfile(grid!);
if (email.value && profile.email !== email.value) {
profile.email = email.value;
}
profileManager.set({ ...profile, mnemonic: mnemonic.value });
await handlePostLogin(grid!, password.value, email.value);
} catch (e) {
if (e instanceof TwinNotExistError) {
isNonActiveMnemonic.value = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ async function login() {
: KeypairType.sr25519;

const grid = await getGrid({ mnemonic: mnemonic, keypairType: keypairType as KeypairType });
await handlePostLogin(grid!, password.value);
profileManager.set({ ...(await loadProfile(grid!)), mnemonic });
await handlePostLogin(grid!, password.value);
emit("closeDialog");
}
} else {
Expand Down
24 changes: 14 additions & 10 deletions packages/playground/src/components/ssh_keys/SshDataDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
<template #default>
<v-card>
<v-toolbar color="primary" class="custom-toolbar">
<p class="mb-5">
SSH-Key Details
</p>
<p class="mb-5">SSH-Key Details</p>
</v-toolbar>
<v-card-text>
<template v-for="[_key, value] of Object.entries(selectedKey).sort()" :key="_key">
Expand All @@ -22,7 +20,10 @@
v-model="currentKey[_key as keyof SSHKeyData]"
:label="_key"
:readonly="_key === 'fingerPrint'"
:rules="[(value: string) => !!value || `${_key} is required.`, _key === 'name' ? validateName(currentKey.name): true]"
:rules="[
(value: string) => !!value || `${_key} is required.`,
_key === 'name' ? validateName(currentKey.name) : true,
]"
/>
</CopyInputWrapper>
<CopyInputWrapper v-else :data="value" #="{ props: copyInputProps }">
Expand All @@ -41,12 +42,8 @@

<v-tooltip text="Key status">
<template #activator="{ props }">
<v-chip v-if="selectedKey.isActive" v-bind="props">
Active
</v-chip>
<v-chip v-else v-bind="props" color="anchor">
Inactive
</v-chip>
<v-chip v-if="selectedKey.isActive" v-bind="props"> Active </v-chip>
<v-chip v-else v-bind="props" color="anchor"> Inactive </v-chip>
</template>
</v-tooltip>

Expand Down Expand Up @@ -74,6 +71,7 @@ import { capitalize, defineComponent, type PropType, ref, watch } from "vue";

import type { SSHKeyData } from "@/types";
import SSHKeysManagement from "@/utils/ssh";
import { isAlphanumericWithSpace } from "@/utils/validators";

export default defineComponent({
name: "SSHDataDialog",
Expand Down Expand Up @@ -133,6 +131,12 @@ export default defineComponent({
if (name === props.selectedKey.name) {
return true;
}
if (isAlphanumericWithSpace("Invalid name")(name) !== true) {
return "Key name must only contain letters, numbers, and spaces within the name.";
}
if (name.length > 30) {
return "Please enter a key name with fewer than 30 characters.";
}
const found = props.allKeys.find(key => key.name === name);
return found ? "You have another key with the same name." : true;
}
Expand Down
26 changes: 18 additions & 8 deletions packages/playground/src/components/ssh_keys/SshFormDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
v-model="$props.open"
max-width="800"
attach="#modals"
@click:outside="() => $emit('close')"
@keydown.esc="() => $emit('close')"
@click:outside="
() => {
if (!generating && !savingKey) $emit('close');
}
"
@keydown.esc="
() => {
if (!generating && !savingKey) $emit('close');
}
"
>
<template #default>
<v-form v-model="isValidForm">
Expand All @@ -23,7 +31,7 @@
>
<v-text-field
v-model="keyName"
hint="Leave this field empty to generate a name automatically, or enter a custom name to save it with your key."
hint="Enter a unique name (letters, numbers, and spaces only, less than 30 characters) to identify your SSH key."
class="mb-4"
hide-details="auto"
label="Name"
Expand Down Expand Up @@ -68,7 +76,7 @@
</v-card-text>

<v-card-actions class="justify-end mb-1 mr-2">
<v-btn color="anchor" text="Close" @click="$emit('close')" />
<v-btn color="anchor" :disabled="generating || savingKey" text="Close" @click="$emit('close')" />

<v-btn
v-if="$props.dialogType === SSHCreationMethod.Generate"
Expand Down Expand Up @@ -103,6 +111,7 @@ import { type Profile, useProfileManager } from "@/stores/profile_manager";
import { SSHCreationMethod, type SSHKeyData } from "@/types";
import { type Balance, loadBalance } from "@/utils/grid";
import SSHKeysManagement from "@/utils/ssh";
import { isAlphanumericWithSpace } from "@/utils/validators";

const props = defineProps({
open: {
Expand Down Expand Up @@ -156,7 +165,7 @@ function generateSSHKey() {
id: keyId,
publicKey: "",
createdAt: sshKeysManagement.formatDate(now),
name: keyName.value,
name: keyName.value.trimEnd(),
isActive: true,
};

Expand All @@ -171,7 +180,7 @@ function createNewSSHKey() {
id: keyId,
publicKey: sshKey.value,
createdAt: sshKeysManagement.formatDate(now),
name: keyName.value,
name: keyName.value.trimEnd(),
isActive: true,
};

Expand All @@ -185,7 +194,7 @@ function createNewSSHKey() {
}
}

createdKey.value.name = keyName.value;
createdKey.value.name = keyName.value.trimEnd();

emits("save", createdKey.value);
}
Expand Down Expand Up @@ -256,8 +265,9 @@ function sshRules(value: any) {

function sshNameRules(value: any) {
return [
(v: string) => !!v || "Key name is required.",
isAlphanumericWithSpace("Key name must only contain letters, numbers, and spaces within the name."),
(v: string) => v.length < 30 || "Please enter a key name with fewer than 30 characters.",
(v: string) => !v.includes(" ") || "Key names cannot include spaces. Please use a name without spaces.",
(v: string) => sshKeysManagement.availableName(v) || "You have another key with the same name.",
];
}
Expand Down
Loading