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

chore: migrate command task wait page over to react [DET-3704] #990

Merged
merged 3 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion master/internal/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@ func (m *Master) Run() error {
elmFiles := [...]fileRoute{
{"/ui", "public/index.html"},
{"/ui/*", "public/index.html"},
{"/wait", "public/wait.html"},
}

elmDirs := [...]fileRoute{
Expand All @@ -460,12 +459,14 @@ func (m *Master) Run() error {
{"/manifest.json", "manifest.json"},
{"/favicon.ico", "favicon.ico"},
{"/favicon.ico", "favicon.ico"},
{"/wait", "wait/index.html"},
}

reactDirs := [...]fileRoute{
{"/favicons", "favicons"},
{"/fonts", "fonts"},
{"/static", "static"},
{"/wait", "wait"},
}

// Apply WebUI routes in order.
Expand Down
29 changes: 29 additions & 0 deletions webui/react/public/wait/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Determined Deep Learning Training Platform" />
<link rel="shortcut icon" type="image/x-icon" href="/favicons/favicon.ico" />
<link rel="apple-touch-icon" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<title>Determined AI</title>
<link href="/wait/wait.css" rel="stylesheet" />
</head>

<body>
<div class="container">
<img class="logo" src="/favicons/favicon-192x192.png" />
<div class="status">
<label>Service State:</label>
<div id="state">Loading...</div>
</div>
<div id="tip"></div>
<div class="spinner">
<div id="spinner" class="icon-spinner spin"></div>
</div>
</div>
<script src="/wait/wait.js" type="text/javascript"></script>
</body>
</html>
94 changes: 94 additions & 0 deletions webui/react/public/wait/wait.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@font-face {
font-display: block;
font-family: 'Objektiv Mk3';
font-style: normal;
font-weight: normal;
src:
url('/fonts/objektiv-mk3-regular.woff2?07a67e') format('woff2'),
url('/fonts/objektiv-mk3-regular.woff?07a67e') format('woff'),
url('/fonts/objektiv-mk3-regular.ttf?07a67e') format('truetype'),
url('/fonts/objektiv-mk3-regular.svg?07a67e#determined-ai') format('svg');
}

@font-face {
font-display: block;
font-family: 'determined-ai';
font-style: normal;
font-weight: normal;
src:
url('/fonts/determined-ai.woff2?n1pu2b') format('woff2'),
url('/fonts/determined-ai.woff?n1pu2b') format('woff'),
url('/fonts/determined-ai.ttf?n1pu2b') format('truetype'),
url('/fonts/determined-ai.svg?n1pu2b#determined-ai') format('svg');
}

/* Wait page specific CSS */

html,
body,
#root {
font-family: 'Objektiv Mk3', Arial, Helvetica, sans-serif;
font-size: 62.5%;
height: 100vh;
margin: 0;
padding: 0;
width: 100vw;
}
body {
align-items: center;
display: flex;
justify-content: center;
}
.container {
align-items: center;
display: flex;
flex-direction: column;
}
.container > *:not(:first-child) {
margin-top: 1.6rem;
}
.logo {
height: 6.4rem;
width: 6.4rem;
}
.status {
display: flex;
font-size: 1.4rem;
}
.status > label {
margin-right: 0.8rem;
}
#tip {
font-size: 1.4rem;
}
.spinner {
height: 4rem;
position: relative;
}
.icon-spinner {
font-family: 'determined-ai', 'Arial', sans-serif;
font-size: 2.4rem;

/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-style: normal;
font-variant: normal;
font-weight: normal;
line-height: 1;
position: relative;
text-transform: none;
}
.icon-spinner::before { content: '\e90f'; }
.spin {
animation: rotate 1s linear infinite;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}

@keyframes rotate {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
100 changes: 100 additions & 0 deletions webui/react/public/wait/wait.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const ERROR_STATES = [ 'TERMINATED', 'TERMINATING' ];

function getUrlVars() {
let vars = {};
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
vars[key] = value;
});
return vars;
}

const titlePrefix = 'Determined AI:';

const tipEl = () => document.getElementById('tip');

const spinner = () => document.getElementById('spinner');

const isFatalError = state => ERROR_STATES.includes(state);

function redirect(url) {
tipEl().innerHTML = 'Redirecting...';
document.title = `${titlePrefix} Redirecting to Service`;
window.location.replace(url);
}

function msgHandler(event, waitType, readyAction) {
let msg = JSON.parse(event.data);
console.log('Message from server ', msg);
if (msg.snapshot) {
const state = msg.snapshot.state;
document.getElementById('state').innerHTML = state;

if (state === 'RUNNING' && msg.snapshot.is_ready) {
document.getElementById('tip').innerHTML = 'Redirecting momentarily..';
document.title = `${titlePrefix} ${state} - Redirecting momentarily`;
// This is a bit redundant given the code at the end of the msgHandler function,
// but it helps avoid panics that occur in the master in the rare instances when
// a redirect happens after the state is marked as RUNNING and before the
// "service_ready_event" flag is raised.
setTimeout(readyAction, 3000);
} else if (isFatalError(state)) {
const waitLabel = waitType === 'notebook' ? 'Notebook' : 'TensorBoard';
document.title = `${titlePrefix} ${state}`;
spinner().className = '';
tipEl().innerHTML = `The requested ${waitLabel} has been killed. Please launch a new one.`;
} else {
document.title = `${titlePrefix} ${state} - Waiting for service`;
}
}
}

// createWsUrl: Given an event url create the corresponding ws url.
function createWsUrl(eventUrl) {
const isFullUrl = /^https?:\/\//i;

if (isFullUrl.test(eventUrl)) {
return eventUrl.replace(/^http/, 'ws');
} else {
// Remove the preceding slash if it is an absolute path.
eventUrl = eventUrl.replace(/^\//, '');
let url = window.location.protocol.replace(/^http/, 'ws');
url += '//' + window.location.host + '/' + eventUrl;
return url;
}
}

function waitForEvents(eventUrl, msgHandler, jumpDest) {
const url = createWsUrl(eventUrl);
const socket = new WebSocket(url);
socket.addEventListener('open', function() {
console.log(`WebSocket is open: ${url}`);
tipEl().innerHTML = 'Waiting for service..';
});
socket.addEventListener('error', function() {
console.error(`WebSocket cannot be opened: ${url}`);
tipEl().innerHTML = 'Service not found';
});
socket.addEventListener('message', function(event) {
const waitType = eventUrl.replace(/^\/?(notebook|tensorboard).*/i, '$1');
msgHandler(event, waitType, function() {
redirect(jumpDest);
});
});
}

(function() {
let eventUrl = decodeURIComponent(getUrlVars()['event']);
let jumpDest = decodeURIComponent(getUrlVars()['jump']);
console.log(`eventUrl: ${eventUrl}`);
console.log(`jumpDest: ${jumpDest}`);
if (typeof eventUrl !== 'string' || eventUrl.length < 2) {
console.error("Wrong or Missing 'event' Parameter in URL");
}
if (typeof jumpDest !== 'string' || jumpDest.length < 2) {
console.error("Wrong or Missing 'jump' Parameter in URL");
}
if (typeof eventUrl === 'string' && eventUrl.length >= 2 &&
typeof jumpDest === 'string' && jumpDest.length >= 2) {
waitForEvents(eventUrl, msgHandler, jumpDest);
}
})();