Skip to content
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

Implementing Multiple SSH Keys with Enhanced Management Features #2463

Merged
merged 30 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b13afe8
WIP: Working on designing the multiple ssh-keys feature.
Mahmoud-Emad Mar 18, 2024
ba36f4d
WIP: Implemented a helper function to generate a name for the generat…
Mahmoud-Emad Mar 18, 2024
e98a0aa
WIP: Added the activation and deletion logic.
Mahmoud-Emad Mar 19, 2024
82c8875
WIP: Worked on deleting all selected keys featrue, now preparing the …
Mahmoud-Emad Mar 19, 2024
cbb65bb
Refactor: Unified UI elements by removing the tonal theme from cards …
Mahmoud-Emad Mar 19, 2024
b03bed0
Refactor: Enhanced the UI by adding another dialog for importing the …
Mahmoud-Emad Mar 19, 2024
52d5365
Refactor: Calculated SSH key fingerprint, consolidated form dialogs, …
Mahmoud-Emad Mar 20, 2024
074163a
Update: Refined the copy input wrapper to conditionally display the c…
Mahmoud-Emad Mar 20, 2024
24799c2
Update: Migrate the old SSH-Key to the new SSH-Key type and set it as…
Mahmoud-Emad Mar 21, 2024
b37adef
Fix: Fixed the issue when saving the ssh-keys, loading the ssh-keys f…
Mahmoud-Emad Mar 24, 2024
4b887f4
Enhancements: Enhanced the way of saving/removing the keys, improved …
Mahmoud-Emad Mar 26, 2024
44187e9
Merge branch 'development' into development_multiple_sshkey
Mahmoud-Emad Mar 26, 2024
1967d1e
Enhancement: Integrated SSH key selection component to facilitate key…
Mahmoud-Emad Mar 26, 2024
c33683c
Enhancement: Substituted the card component with a v-chip component i…
Mahmoud-Emad Mar 26, 2024
f09e8df
Work Completed: Merged SSH key management functionality into all appl…
Mahmoud-Emad Mar 27, 2024
664dcc7
Fix: Fix playground build.
Mahmoud-Emad Mar 27, 2024
239ab07
Remove key fragments after setting key with new value
AhmedHanafy725 Mar 27, 2024
bb51347
Enhancement: Download the private key when generate it, fix some issu…
Mahmoud-Emad Mar 27, 2024
46b7b97
Reuse clean fragments in remove method
AhmedHanafy725 Mar 27, 2024
902046d
Merge pull request #2503 from threefoldtech/development_clean_kvstore
AhmedHanafy725 Mar 27, 2024
fe5b211
Update: Migrate SSH_Table_View to V3 instead of V3
Mahmoud-Emad Mar 31, 2024
6657440
Update: Migrate SSH components to V3 instead of V3
Mahmoud-Emad Mar 31, 2024
996736a
Merge branch 'development' into development_multiple_sshkey
Mahmoud-Emad Mar 31, 2024
e674520
Update: Migrate SSH components to V3 instead of V3
Mahmoud-Emad Mar 31, 2024
54cc569
Fix: fix playground build.
Mahmoud-Emad Mar 31, 2024
8045139
Merge branch 'development' into development_multiple_sshkey
Mahmoud-Emad Apr 2, 2024
4ce8312
- fix: checkbox handler wasn't correctly named
MohamedElmdary Apr 2, 2024
ba6cb05
Enhancements: Removed unused btn.
Mahmoud-Emad Apr 2, 2024
7b9b2c9
Merge branch 'development_multiple_sshkey' of https://github.com/thre…
Mahmoud-Emad Apr 2, 2024
35110eb
- style: Move alert outside component
MohamedElmdary Apr 2, 2024
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
31 changes: 21 additions & 10 deletions packages/grid_client/src/storage/tfkvstore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,33 @@ class TFKVStoreBackend implements BackendStorageInterface {
this.client = new TFClient(url, mnemonic, storeSecret, keypairType);
}

private async cleanFragments(key: string, start: number) {
let k = `${key}.${start}`;
let value = await this.client.kvStore.get({ key: k });
if (!value) {
return [];
}
const extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[] = [];
while (value) {
extrinsics.push(await this.client.kvStore.delete({ key: k }));
start++;
k = `${key}.${start}`;
value = await this.client.kvStore.get({ key: k });
}
return extrinsics;
}

@crop
async set(key: string, value: string) {
if (!value || value === '""') {
return await this.remove(key);
}
const extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[] = [];
let extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[] = [];
const splits = this.split(key, value);
for (const k of Object.keys(splits)) {
extrinsics.push(await this.client.kvStore.set({ key: k, value: splits[k] }));
}
extrinsics = extrinsics.concat(await this.cleanFragments(key, Object.keys(splits).length));
return extrinsics;
}

Expand Down Expand Up @@ -52,15 +69,9 @@ class TFKVStoreBackend implements BackendStorageInterface {
if (!value) {
return;
}
let i = 0;
let val = value;
const extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[] = [];
while (val) {
extrinsics.push(await this.client.kvStore.delete({ key }));
i++;
key = `${key}.${i}`;
val = await this.client.kvStore.get({ key });
}

const extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[] = await this.cleanFragments(key, 1);
extrinsics.push(await this.client.kvStore.delete({ key }));
return extrinsics;
}

Expand Down
12 changes: 8 additions & 4 deletions packages/playground/src/components/copy_input_wrapper.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<template>
<slot
:props="{
'append-inner-icon': 'mdi-content-copy',
'onClick:append-inner': copy,
}"
:props="
props.data
? {
'append-inner-icon': 'mdi-content-copy',
'onClick:append-inner': copy,
}
: {}
"
></slot>
</template>

Expand Down
6 changes: 5 additions & 1 deletion packages/playground/src/components/input_tooltip.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="d-flex" v-if="!disabled">
<v-tooltip :text="tooltip || 'None!'" :location="location">
<v-tooltip :text="tooltip || 'None!'" :location="location" :width="width ? width : ''">
<template #activator="{ props }">
{{ getPropsRef(props) }}
<div class="d-flex" :class="{ 'w-100': !inline, 'align-center': alignCenter }">
Expand Down Expand Up @@ -51,6 +51,10 @@ export default {
type: String as PropType<VTooltip["location"]>,
required: false,
},
width: {
type: String,
required: false,
},
disabled: Boolean,
},
setup() {
Expand Down
191 changes: 191 additions & 0 deletions packages/playground/src/components/ssh_keys/ManageSshDeployemnt.vue
Mahmoud-Emad marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<template>
<v-card class="" variant="tonal">
<v-card-title>
<v-icon>mdi-key-chain</v-icon>
Manage SSH keys
</v-card-title>
<v-card-text>
SSH grants secure remote access to your deployed machine for seamless management and execution of commands.
<v-alert v-if="selectedKeys.length === 0" type="warning" class="mt-2">
Attention: It appears that no SSH keys have been selected. In order to access your deployment, you must send at
least one SSH key. You can manage your SSH keys from the
<router-link :to="DashboardRoutes.Deploy.SSHKey">SSH keys management page</router-link> and add more as needed.
</v-alert>
</v-card-text>

<VDivider />

<v-card-actions>
<VSpacer />
<v-btn color="primary" variant="flat" @click="openManageDialog = true" class="mr-2">Manage SSH keys</v-btn>
</v-card-actions>
</v-card>

<v-dialog v-model="openManageDialog" max-width="850">
<v-card>
<v-toolbar color="primary" class="custom-toolbar">
<p class="mb-5">
<v-icon>mdi-cog-sync</v-icon>
Manage SSH keys
</p>
</v-toolbar>

<v-card-text>
<v-alert type="info" class="mb-5">
The keys you've chosen will be forwarded to your deployment. To make changes, simply tap on the key you wish
to cancel or add.
</v-alert>

<v-row>
<v-tooltip
v-for="_key of sshKeys"
:key="_key.id"
:text="selectedKeys.includes(_key) ? 'Selected' : 'Not selected'"
location="bottom"
>
<template #activator="{ props }">
<v-chip
class="pa-5 ml-5 mt-5"
:variant="selectedKeys.includes(_key) ? 'flat' : 'outlined'"
:color="selectedKeys.includes(_key) ? 'primary' : 'white'"
v-bind="props"
@click="selectKey(_key)"
>
<div class="d-flex justify-center">
<v-icon>mdi-key-variant</v-icon>
</div>
<div class="d-flex justify-center">
<p class="ml-2">
{{ capitalize(_key.name) }}
</p>
</div>
</v-chip>
</template>
</v-tooltip>
</v-row>
</v-card-text>

<v-card-actions>
<v-spacer></v-spacer>
<v-btn class="mt-2 mb-2 mr-2" variant="outlined" color="white" text="Close" @click="openManageDialog = false" />
</v-card-actions>
</v-card>
</v-dialog>

<!-- View SSH Key -->
<ssh-data-dialog :open="isViewSSHKey" :selected-key="selectedKey" @close="onCloseSelectKey" />
</template>

<script lang="ts">
import { noop } from "lodash";
import { capitalize, defineComponent, getCurrentInstance, nextTick, onMounted, ref, watch } from "vue";
import { onUnmounted } from "vue";

import SshDataDialog from "@/components/ssh_keys/SshDataDialog.vue";
import { useForm, ValidatorStatus } from "@/hooks/form_validator";
import type { InputValidatorService } from "@/hooks/input_validator";
import { DashboardRoutes } from "@/router/routes";
import { useProfileManager } from "@/stores";
import type { SSHKeyData } from "@/types";

export default defineComponent({
name: "ManageSshDeployemnt",
emits: ["selectedKeys"],
components: {
SshDataDialog,
},

setup(_, { emit }) {
const defaultKeyData = { createdAt: "", id: 0, publicKey: "", name: "", isActive: false };
const openManageDialog = ref<boolean>(false);
const profileManager = useProfileManager();
const sshKeys = profileManager.profile?.ssh as SSHKeyData[];
const selectedKey = ref<SSHKeyData>(defaultKeyData);
const selectedKeys = ref<SSHKeyData[]>([]);
const isViewSSHKey = ref<boolean>(false);

// Each key will be added then add `\n` as a new line.
const selectedKeysString = ref<string>("");

onMounted(() => {
selectedKeys.value = sshKeys.filter(_key => _key.isActive === true);
handleKeys();
emit("selectedKeys", selectedKeysString.value);
});

function selectKey(key: SSHKeyData) {
if (selectedKeys.value.includes(key)) {
const index = selectedKeys.value.indexOf(key);
if (index !== -1) {
selectedKeys.value.splice(index, 1);
}
} else {
selectedKeys.value.push(key);
}
handleKeys();
emit("selectedKeys", selectedKeysString.value);
}

function onSelectKey(key: SSHKeyData) {
selectedKey.value = key;
isViewSSHKey.value = true;
}

function onCloseSelectKey() {
isViewSSHKey.value = false;
nextTick(() => {
selectedKey.value = defaultKeyData;
});
}

function handleKeys() {
selectedKeysString.value = selectedKeys.value.map(_key => _key.publicKey).join("\n\n");
}

/* interact with form_validator */
const { uid } = getCurrentInstance() as { uid: number };
const form = useForm();

const fakeService: InputValidatorService = {
validate: () => Promise.resolve(true),
setStatus: noop,
reset: noop,
status: ValidatorStatus.Init,
error: null,
};

onMounted(() => form?.register(uid, fakeService));
onUnmounted(() => form?.unregister(uid));

watch(
() => selectedKeys.value.length,
num => {
form?.updateStatus(uid, num === 0 ? ValidatorStatus.Invalid : ValidatorStatus.Valid);
},
{ immediate: true },
);

return {
openManageDialog,
sshKeys,
selectedKeys,
selectedKey,
isViewSSHKey,
defaultKeyData,
selectedKeysString,
DashboardRoutes,

capitalize,
onSelectKey,
onCloseSelectKey,
selectKey,
};
},
});
</script>

<style>
.cursor-pointer {
cursor: pointer;
}
</style>
103 changes: 103 additions & 0 deletions packages/playground/src/components/ssh_keys/SshDataDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<template>
<v-dialog
@click:outside="() => $emit('close')"
@keydown.esc="() => $emit('close')"
v-model="$props.open"
max-width="750"
>
<template v-slot:default>
<v-card>
<v-toolbar color="primary" class="custom-toolbar">
<p class="mb-5">SSH-Key Detials</p>
</v-toolbar>

<v-card-text>
<template v-for="[_key, value] of Object.entries(selectedKey).sort()" :key="_key">
<template v-if="!notNeededFields.includes(_key)">
<CopyInputWrapper v-if="_key !== 'publicKey'" :data="value" #="{ props: copyInputProps }">
<v-text-field v-bind="{ ...copyInputProps }" :label="_key" :model-value="value" :readonly="true" />
</CopyInputWrapper>
<CopyInputWrapper v-else :data="value" #="{ props: copyInputProps }">
<v-textarea
:class="value.length ? 'ssh-key' : ''"
:model-value="value"
:readonly="true"
label="Public SSH Key"
no-resize
:spellcheck="false"
v-bind="{ ...copyInputProps }"
/>
</CopyInputWrapper>
</template>
</template>

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

<v-tooltip text="Created at">
<template #activator="{ props }">
<v-chip v-bind="props" class="ml-2" color="primary">{{ selectedKey.createdAt }}</v-chip>
</template>
</v-tooltip>
</v-card-text>

<v-card-actions class="mb-3 custom-actions">
<v-spacer></v-spacer>
<div class="mt-2">
<v-btn color="white" variant="outlined" text="Close" @click="$emit('close')"></v-btn>
</div>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>

<script lang="ts">
import { capitalize, defineComponent, type PropType } from "vue";

import type { SSHKeyData } from "@/types";

export default defineComponent({
name: "SSHDataDialog",
emits: ["close"],
props: {
open: {
type: Boolean,
required: true,
},
selectedKey: {
type: Object as PropType<SSHKeyData>,
required: true,
},
},
setup() {
const notNeededFields = ["id", "activating", "deleting", "isActive", "createdAt"];
return {
notNeededFields,
capitalize,
};
},
});
</script>

<style>
.custom-toolbar {
height: 2.5rem !important;
padding-left: 10px;
}
.ssh-key .v-field__input {
height: 230px !important;
}
.custom-actions {
border-top: 1px solid rgb(101 99 99);
display: flex;
justify-content: center;
margin-right: 15px;
margin-left: 15px;
margin-top: 15px;
}
</style>
Loading
Loading