Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 62 additions & 0 deletions ui/src/components/PrivateKeys/PrivateKeySelectWithAdd.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<div>
<v-select
v-model="selectedPrivateKeyName"
:items="privateKeysNames"
:list-props="{ class: 'py-0' }"
label="Private Key"
hint="Select a private key file for authentication"
persistent-hint
data-test="private-keys-select"
>
<template #append-item>
<v-divider />
<v-list-item
data-test="add-private-key-btn"
@click="showPrivateKeyAdd = true"
>
<template #prepend>
<v-icon
color="primary"
icon="mdi-plus"
/>
</template>
<v-list-item-title class="text-primary">
Add New Private Key
</v-list-item-title>
</v-list-item>
</template>
</v-select>

<PrivateKeyAdd
v-model="showPrivateKeyAdd"
@update="handlePrivateKeyAdded"
/>
</div>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import PrivateKeyAdd from "@/components/PrivateKeys/PrivateKeyAdd.vue";
import usePrivateKeysStore from "@/store/modules/private_keys";
import { IPrivateKey } from "@/interfaces/IPrivateKey";

const emit = defineEmits<{ "key-added": [] }>();

const selectedPrivateKeyName = defineModel<string>({ required: true });
const privateKeysStore = usePrivateKeysStore();
const showPrivateKeyAdd = ref(false);

const privateKeysNames = computed(() => privateKeysStore.privateKeys.map((item: IPrivateKey) => item.name));

const handlePrivateKeyAdded = () => {
privateKeysStore.getPrivateKeyList();
const newestKey = privateKeysStore.privateKeys[privateKeysStore.privateKeys.length - 1];
if (newestKey) {
selectedPrivateKeyName.value = newestKey.name;
emit("key-added");
}
};

defineExpose({ selectedPrivateKeyName });
</script>
13 changes: 3 additions & 10 deletions ui/src/components/Terminal/TerminalLoginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,10 @@
@update:model-value="togglePassphraseField"
/>

<v-select
<PrivateKeySelectWithAdd
v-if="authenticationMethod === TerminalAuthMethods.PrivateKey"
v-model="selectedPrivateKeyName"
:items="privateKeysNames"
item-text="name"
item-value="data"
label="Private Key"
hint="Select a private key file for authentication"
persistent-hint
data-test="private-keys-select"
@update:model-value="togglePassphraseField"
@key-added="togglePassphraseField"
/>

<v-text-field
Expand Down Expand Up @@ -127,6 +120,7 @@ import * as yup from "yup";
import { useField } from "vee-validate";
import FormDialog from "@/components/Dialogs/FormDialog.vue";
import SSHIDHelper from "./SSHIDHelper.vue";
import PrivateKeySelectWithAdd from "@/components/PrivateKeys/PrivateKeySelectWithAdd.vue";
import { LoginFormData, TerminalAuthMethods } from "@/interfaces/ITerminal";
import { IPrivateKey } from "@/interfaces/IPrivateKey";
import usePrivateKeysStore from "@/store/modules/private_keys";
Expand All @@ -146,7 +140,6 @@ const { privateKeys } = usePrivateKeysStore();
const authenticationMethod = ref(TerminalAuthMethods.Password);
const showPassword = ref(false);
const selectedPrivateKeyName = ref(privateKeys[0]?.name || "");
const privateKeysNames = privateKeys.map((item: IPrivateKey) => item.name);
const showPassphraseField = ref(false);
const showTerminalHelper = ref(false);

Expand Down
83 changes: 83 additions & 0 deletions ui/tests/components/PrivateKeys/PrivateKeySelectWithAdd.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { setActivePinia, createPinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createVuetify } from "vuetify";
import { flushPromises, mount, VueWrapper } from "@vue/test-utils";
import PrivateKeySelectWithAdd from "@/components/PrivateKeys/PrivateKeySelectWithAdd.vue";
import { SnackbarPlugin } from "@/plugins/snackbar";
import usePrivateKeysStore from "@/store/modules/private_keys";
import { nextTick } from "vue";

type PrivateKeySelectWithAddWrapper = VueWrapper<InstanceType<typeof PrivateKeySelectWithAdd>>;

const mockPrivateKeys = [
{ id: 1, name: "test-key-1", data: "private-key-data-1", hasPassphrase: true, fingerprint: "fingerprint-1" },
{ id: 2, name: "test-key-2", data: "private-key-data-2", hasPassphrase: false, fingerprint: "fingerprint-2" },
{ id: 3, name: "test-key-3", data: "private-key-data-3", hasPassphrase: false, fingerprint: "fingerprint-3" },
];

describe("Private Key Select With Add", () => {
let wrapper: PrivateKeySelectWithAddWrapper;
setActivePinia(createPinia());
const privateKeysStore = usePrivateKeysStore();
const vuetify = createVuetify();

beforeEach(() => {
privateKeysStore.privateKeys = mockPrivateKeys;

wrapper = mount(PrivateKeySelectWithAdd, {
global: {
plugins: [vuetify, SnackbarPlugin],
stubs: {
"v-file-upload": true,
"v-file-upload-item": true,
},
},
props: { modelValue: "test-key-1" },
});
});

it("Renders the private key select", () => {
const select = wrapper.find('[data-test="private-keys-select"]');
expect(select.exists()).toBe(true);
});

it("Displays all private keys in the select", () => {
const select = wrapper.findComponent({ name: "VSelect" });
expect(select.props("items")).toEqual(["test-key-1", "test-key-2", "test-key-3"]);
});

it("Auto-selects newly added key and emits key-added event", async () => {
const newKey = { id: 4, name: "new-test-key", data: "new-key-data", hasPassphrase: false, fingerprint: "new-fingerprint" };

const getPrivateKeyListSpy = vi.spyOn(privateKeysStore, "getPrivateKeyList").mockImplementation(() => {
privateKeysStore.privateKeys = [...mockPrivateKeys, newKey];
});

const privateKeyAdd = wrapper.findComponent({ name: "PrivateKeyAdd" });
await privateKeyAdd.vm.$emit("update");
await nextTick();
await flushPromises();

expect(getPrivateKeyListSpy).toHaveBeenCalled();
expect(wrapper.emitted("key-added")).toBeTruthy();
expect(wrapper.vm.selectedPrivateKeyName).toBe("new-test-key");
});

it("Handles empty private keys list", async () => {
privateKeysStore.privateKeys = [];

await nextTick();

const select = wrapper.findComponent({ name: "VSelect" });
expect(select.props("items")).toEqual([]);
});

it("Updates model value when selecting a key", async () => {
const select = wrapper.findComponent({ name: "VSelect" });
await select.setValue("test-key-2");
await flushPromises();

expect(wrapper.emitted("update:modelValue")).toBeTruthy();
expect(wrapper.emitted("update:modelValue")?.[0]).toEqual(["test-key-2"]);
});
});
3 changes: 2 additions & 1 deletion ui/tests/components/Terminal/TerminalLoginForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TerminalLoginForm from "@/components/Terminal/TerminalLoginForm.vue";
import { IPrivateKey } from "@/interfaces/IPrivateKey";
import { TerminalAuthMethods } from "@/interfaces/ITerminal";
import usePrivateKeysStore from "@/store/modules/private_keys";
import { SnackbarPlugin } from "@/plugins/snackbar";

const mockPrivateKeys: Array<IPrivateKey> = [
{ id: 1, name: "test-key-1", data: "private-key-data-1", hasPassphrase: true, fingerprint: "fingerprint-1" },
Expand All @@ -25,7 +26,7 @@ describe("Terminal Login Form", () => {

wrapper = mount(TerminalLoginForm, {
global: {
plugins: [vuetify],
plugins: [vuetify, SnackbarPlugin],
},
props: {
modelValue: true,
Expand Down