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

Cleanup the UI to be much nicer #78

Merged
merged 14 commits into from
Feb 5, 2024
30 changes: 30 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ["eslint:recommended"],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: [],
rules: {
"no-unused-vars": ["error", { args: "after-used" }],
},
ignorePatterns: [
"jupyter_remote_desktop_proxy/static/dist/**",
"webpack.config.js",
],
};
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ repos:
hooks:
- id: flake8

# Lint: JS code
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v8.56.0" # Use the sha / tag you want to point at
hooks:
- id: eslint
files: \.jsx?$
types: [file]
exclude: jupyter_remote_desktop_proxy/static/dist

# Content here is mostly copied from other locations, so lets not make
# formatting changes in it.
exclude: share
Expand Down
97 changes: 97 additions & 0 deletions js/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed
* under the 2-clause BSD license
*/

html {
/**
Colors from https://github.com/jupyter/design/blob/main/brandguide/brand_guide.pdf
**/
--jupyter-main-brand-color: #f37626;
--jupyter-dark-grey: #4d4d4d;
--jupyter-medium-dark-grey: #616161;
--jupyter-medium-grey: #757575;
--jupyter-grey: #9e9e9e;

--topbar-height: 32px;

/* Use Jupyter Brand fonts */
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}

body {
height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--jupyter-medium-dark-grey);
}

#top-bar {
background-color: var(--jupyter-dark-grey);
color: white;
border-bottom: 1px white;
display: flex;
align-items: center;
}

#logo {
padding: 0 24px;
}

#logo img {
height: 24px;
}

#menu {
display: flex;
font-weight: bold;
margin-left: auto;
font-size: 12px;
}

#menu li {
border-right: 1px var(--jupyter-grey) solid;
padding: 12px 0px;
}

#menu li:last-child {
border-right: 0;
}

#menu a {
color: white;
text-decoration: none;
padding: 12px 8px;
}

#menu a:hover,
#menu a.active {
background-color: var(--jupyter-medium-grey);
}

li#status-container {
padding-right: 8px;
}

#status-label {
font-weight: normal;
}

#screen {
flex: 1;
/* fill remaining space */
overflow: hidden;
}

/* Clipboard */
#clipboard-content {
display: flex;
flex-direction: column;
padding: 4px;
gap: 4px;
}

#clipboard-text {
min-width: 500px;
max-width: 100%;
}
125 changes: 27 additions & 98 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
* under the 2-clause BSD license
*/

import "reset-css";
import "./index.css";

// RFB holds the API to connect and communicate with a VNC server
import RFB from "@novnc/novnc/core/rfb";

let rfb;
let desktopName;
import { setupTooltip } from "./tooltip.js";

// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
status("Connected to " + desktopName);
// When this function is called we have successfully connected to a server
function connectedToServer() {
status("Connected");
}

// This function is called when we are disconnected
Expand All @@ -24,119 +25,47 @@ function disconnectedFromServer(e) {
}
}

// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password Required:");
rfb.sendCredentials({ password: password });
}

// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = e.detail.name;
}

// Since most operating systems will catch Ctrl+Alt+Del
// before they get a chance to be intercepted by the browser,
// we provide a way to emulate this key sequence.
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}

// Show a status text in the top bar
function status(text) {
document.getElementById("status").textContent = text;
}

// This function extracts the value of one variable from the
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp(".*[?&]" + name + "=([^&#]*)"),
match = document.location.href.match(re);

if (match) {
// We have to decode the URL since want the cleartext value
return decodeURIComponent(match[1]);
}

return defaultValue;
}

document.getElementById("sendCtrlAltDelButton").onclick = sendCtrlAltDel;

// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable("host", window.location.hostname);
let port = readQueryVariable("port", window.location.port);
const password = readQueryVariable("password");

const path = readQueryVariable(
"path",
window.location.pathname.replace(/[^/]*$/, "").substring(1) + "websockify",
);

// | | | | | |
// | | | Connect | | |
// v v v v v v

status("Connecting");

// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = "wss";
} else {
url = "ws";
}
url += "://" + host;
if (port) {
url += ":" + port;
}
url += "/" + path;
// Construct the websockify websocket URL we want to connect to
let websockifyUrl = new URL("websockify", window.location);
websockifyUrl.protocol = window.location.protocol === "https:" ? "wss" : "ws";

// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById("screen"), url, {
credentials: { password: password },
});
const rfb = new RFB(
document.getElementById("screen"),
websockifyUrl.toString(),
{},
);

// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);

// Set parameters that can be changed on an active connection
rfb.viewOnly = readQueryVariable("view_only", false);
// Scale our viewport so the user doesn't have to scroll
rfb.scaleViewport = true;

rfb.scaleViewport = readQueryVariable("scale", true);
// Use a CSS variable to set background color
rfb.background = "var(--jupyter-medium-dark-grey)";

// Clipboard
function toggleClipboardPanel() {
document
.getElementById("noVNC_clipboard_area")
.classList.toggle("noVNC_clipboard_closed");
}
document
.getElementById("noVNC_clipboard_button")
.addEventListener("click", toggleClipboardPanel);

function clipboardReceive(e) {
document.getElementById("noVNC_clipboard_text").value = e.detail.text;
document.getElementById("clipboard-text").value = e.detail.text;
}
rfb.addEventListener("clipboard", clipboardReceive);

function clipboardSend() {
const text = document.getElementById("noVNC_clipboard_text").value;
const text = document.getElementById("clipboard-text").value;
rfb.clipboardPasteFrom(text);
}
document
.getElementById("noVNC_clipboard_text")
.getElementById("clipboard-text")
.addEventListener("change", clipboardSend);

setupTooltip(
document.getElementById("clipboard-button"),
document.getElementById("clipboard-container"),
);
24 changes: 24 additions & 0 deletions js/tooltip.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.hidden {
display: none !important;
}

.tooltip-container {
overflow: visible; /* Needed for the arrow to show up */
width: max-content;
position: absolute;
top: 0;
left: 0;
background: white;
color: var(--jupyter-dark-grey);
padding: 6px;
border-radius: 4px;
font-size: 90%;
}

.arrow {
position: absolute;
background: white;
width: 8px;
height: 8px;
transform: rotate(45deg);
}
57 changes: 57 additions & 0 deletions js/tooltip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Setup simplest popover possible to provide popovers.
*
* Mostly follows https://floating-ui.com/docs/tutorial
*/
import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom";
import "./tooltip.css";

/**
* Setup trigger element to toggle showing / hiding tooltip element
* @param {Element} trigger
* @param {Element} tooltip
*/
export function setupTooltip(trigger, tooltip) {
const arrowElement = tooltip.querySelector(".arrow");
function updatePosition() {
computePosition(trigger, tooltip, {
placement: "bottom",
middleware: [
offset(6),
flip(),
shift({ padding: 5 }),
arrow({ element: arrowElement }),
],
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});

// Accessing the data
const { x: arrowX, y: arrowY } = middlewareData.arrow;

const staticSide = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
}[placement.split("-")[0]];

Object.assign(arrowElement.style, {
left: arrowX != null ? `${arrowX}px` : "",
top: arrowY != null ? `${arrowY}px` : "",
right: "",
bottom: "",
[staticSide]: "-4px",
});
});
}

trigger.addEventListener("click", (e) => {
tooltip.classList.toggle("hidden");
trigger.classList.toggle("active");
updatePosition();
e.preventDefault();
});
}
Loading
Loading