Skip to content

fix(ui): fix TerminalDialog layout #4745

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

Merged
merged 8 commits into from
Apr 23, 2025
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
26 changes: 13 additions & 13 deletions ui/src/components/Terminal/TerminalDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@
>
Terminal
<v-spacer />
<v-icon @click="close()" data-test="close-btn" class="bg-primary" size="24">mdi-close</v-icon>
<v-icon v-if="!showLoginForm" @click="close()" data-test="close-terminal-btn" size="24">mdi-close</v-icon>
</v-card-title>

<div class="ma-0 pa-0 w-100 fill-height position-relative" v-if="!showLoginForm">
<div ref="terminal" class="terminal" />
</div>

<div class="mt-2" v-if="showLoginForm">

<v-card-text>
<v-window>
<v-window-item :value="AuthMethods.Password">
<v-form lazy-validation>
<v-form @submit.prevent="submitForm">
<v-container>
<v-row>
<v-col>
Expand Down Expand Up @@ -75,7 +74,7 @@
label="Private Key"
hint="Select a private key file for authentication"
persistent-hint
data-test="privatekeys-select"
data-test="private-keys-select"
/>
<v-text-field
color="primary"
Expand All @@ -96,19 +95,20 @@
</v-row>
</v-container>

<v-card-actions>
<v-spacer />
<v-card-actions class="mt-4 d-flex justify-end">
<v-btn
@click="close"
data-test="cancel-btn"
>
Cancel
</v-btn>
<v-btn
type="button"
type="submit"
color="primary"
class="mt-4"
variant="flat"
data-test="connect2-btn"
@click="submitForm"
data-test="submit-btn"
>
Connect
</v-btn>
<v-spacer />
</v-card-actions>
</v-form>
</v-window-item>
Expand Down Expand Up @@ -412,7 +412,7 @@ onBeforeUnmount(() => {
window.removeEventListener("keyup", handleEscKey);
});

defineExpose({ open, showTerminal, showLoginForm, encodeURLParams, connect, privateKey, xterm, fitAddon, ws, close });
defineExpose({ open, showTerminal, showLoginForm, encodeURLParams, submitForm, connect, privateKey, xterm, fitAddon, ws, close });
</script>

<style lang="scss" scoped>
Expand Down
171 changes: 47 additions & 124 deletions ui/tests/components/Terminal/TerminalDialog.spec.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1,27 @@
import { flushPromises, DOMWrapper, mount, VueWrapper } from "@vue/test-utils";
import { createVuetify } from "vuetify";
import MockAdapter from "axios-mock-adapter";
import { expect, describe, it, beforeEach } from "vitest";
import { expect, describe, it, beforeEach, vi } from "vitest";
import { nextTick, watch } from "vue";
import { store, key } from "@/store";
import TerminalDialog from "@/components/Terminal/TerminalDialog.vue";
import { router } from "@/router";
import { namespacesApi, devicesApi } from "@/api/http";
import { SnackbarPlugin } from "@/plugins/snackbar";

const node = document.createElement("div");
node.setAttribute("id", "app");
document.body.appendChild(node);

const devices = [
{
uid: "a582b47a42d",
name: "39-5e-2a",
identity: {
mac: "00:00:00:00:00:00",
},
info: {
id: "linuxmint",
pretty_name: "Linux Mint 19.3",
version: "",
},
public_key: "----- PUBLIC KEY -----",
tenant_id: "fake-tenant-data",
last_seen: "2020-05-20T18:58:53.276Z",
online: false,
namespace: "user",
status: "accepted",
tags: ["test"],
},
{
uid: "a582b47a42e",
name: "39-5e-2b",
identity: {
mac: "00:00:00:00:00:00",
},
info: {
id: "linuxmint",
pretty_name: "Linux Mint 19.3",
version: "",
},
public_key: "----- PUBLIC KEY -----",
tenant_id: "fake-tenant-data",
last_seen: "2020-05-20T19:58:53.276Z",
online: true,
namespace: "user",
status: "accepted",
tags: ["test2"],
},
];

const members = [
{
id: "xxxxxxxx",
username: "test",
role: "owner",
},
];

const namespaceData = {
name: "test",
owner: "xxxxxxxx",
tenant_id: "fake-tenant-data",
members,
max_devices: 3,
devices_count: 3,
devices: 2,
created_at: "",
};

const authData = {
status: "",
token: "",
user: "test",
name: "test",
tenant: "fake-tenant-data",
email: "[email protected]",
id: "xxxxxxxx",
role: "owner",
};

const stats = {
registered_devices: 3,
online_devices: 1,
active_sessions: 0,
pending_devices: 0,
rejected_devices: 0,
};

describe("Terminal Dialog", async () => {
let wrapper: VueWrapper<InstanceType<typeof TerminalDialog>>;

const vuetify = createVuetify();

let mockNamespace: MockAdapter;
let mockDevices: MockAdapter;

beforeEach(async () => {
const el = document.createElement("div");
document.body.appendChild(el);

localStorage.setItem("tenant", "fake-tenant-data");

mockNamespace = new MockAdapter(namespacesApi.getAxios());
mockDevices = new MockAdapter(devicesApi.getAxios());
mockNamespace.onGet("http://localhost:3000/api/namespaces/fake-tenant-data").reply(200, namespaceData);
mockDevices.onGet("http://localhost:3000/api/devices?filter=&page=1&per_page=10&status=accepted").reply(200, devices);
mockDevices.onGet("http://localhost:3000/api/stats").reply(200, stats);

store.commit("auth/authSuccess", authData);
store.commit("namespaces/setNamespace", namespaceData);

wrapper = mount(TerminalDialog, {
global: {
plugins: [[store, key], vuetify, router, SnackbarPlugin],
config: {
errorHandler: () => { /* ignore global error handler */ },
},
plugins: [[store, key], vuetify, router],
},
props: {
uid: devices[0].uid,
uid: "a582b47a42d",
enableConnectButton: true,
enableConsoleIcon: true,
online: true,
show: true,
},
});
});
Expand All @@ -136,36 +34,30 @@ describe("Terminal Dialog", async () => {
expect(wrapper.html()).toMatchSnapshot();
});

it("Data is defined", () => {
expect(wrapper.vm.$data).toBeDefined();
});

it("Renders the component table", async () => {
await wrapper.setProps({ enableConnectButton: true, enableConsoleIcon: true, online: true, show: true });

expect(wrapper.find('[data-test="connect-btn"]').exists()).toBe(true);

it("Renders the components", async () => {
const dialog = new DOMWrapper(document.body);
const connectBtn = wrapper.find('[data-test="connect-btn"]');

await flushPromises();

await wrapper.findComponent('[data-test="connect-btn"]').trigger("click");
expect(connectBtn.exists()).toBe(true);
await connectBtn.trigger("click");

expect(dialog.find('[data-test="terminal-card"]').exists()).toBe(true);
expect(dialog.find('[data-test="close-btn"]').exists()).toBe(true);
expect(dialog.find('[data-test="username-field"]').exists()).toBe(true);
expect(dialog.find('[data-test="password-field"]').exists()).toBe(true);
expect(dialog.find('[data-test="connect2-btn"]').exists()).toBe(true);
expect(dialog.find('[data-test="cancel-btn"]').exists()).toBe(true);
expect(dialog.find('[data-test="submit-btn"]').exists()).toBe(true);
expect(dialog.find('[data-test="auth-method-select"]').exists()).toBe(true);

await wrapper.findComponent('[data-test="auth-method-select"]').setValue("Private Key");
await flushPromises();

expect(dialog.find('[data-test="password-field"]').exists()).toBe(false);
expect(dialog.find('[data-test="privatekeys-select"]').exists()).toBe(true);
expect(dialog.find('[data-test="private-keys-select"]').exists()).toBe(true);
});

it("sets showLoginForm to true when showTerminal changes to true", async () => {
await watch(() => wrapper.vm.showTerminal, (value) => {
watch(() => wrapper.vm.showTerminal, (value) => {
if (value) wrapper.vm.showLoginForm = true;
});

Expand All @@ -176,6 +68,37 @@ describe("Terminal Dialog", async () => {
expect(wrapper.vm.showLoginForm).toBe(true);
});

it("shows X button when terminal is open", async () => {
const dialog = new DOMWrapper(document.body);
const connectBtn = wrapper.find('[data-test="connect-btn"]');
await connectBtn.trigger("click");

wrapper.vm.showLoginForm = false;

await flushPromises();

const closeBtn = dialog.find('[data-test="close-terminal-btn"]');
expect(closeBtn.exists()).toBe(true);
});

it("submits form when Enter is pressed", async () => {
const submitFormSpy = vi.spyOn(wrapper.vm, "submitForm").mockImplementation(vi.fn());
const dialog = new DOMWrapper(document.body);
const connectBtn = wrapper.find('[data-test="connect-btn"]');

await connectBtn.trigger("click");

const usernameField = dialog.find('[data-test="username-field"] input');
const passwordField = dialog.find('[data-test="password-field"] input');

await usernameField.setValue("testuser");
await passwordField.setValue("testpass");

passwordField.trigger("keydown.enter.prevent");
await nextTick();
expect(submitFormSpy).toBeTruthy();
});

it("encodes URL params correctly", () => {
const params = { key1: "value1", key2: "value2" };
const encodedParams = wrapper.vm.encodeURLParams(params);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Terminal Dialog > Renders the component 1`] = `
"<div data-v-abcb598c="">
<!--v-if-->
"<div data-v-abcb598c=""><button data-v-abcb598c="" type="button" class="v-btn v-theme--light text-success v-btn--density-comfortable v-btn--size-default v-btn--variant-outlined" data-test="connect-btn"><span class="v-btn__overlay"></span><span class="v-btn__underlay"></span>
<!----><span class="v-btn__content" data-no-activator="">Connect</span>
<!---->
<!---->
</button>
<!---->
<!---->
</div>"
Expand Down