Skip to content

Commit

Permalink
Merge branch 'livebook-dev:main' into feature/ISSUE-1743
Browse files Browse the repository at this point in the history
  • Loading branch information
ByeongUkChoi authored May 1, 2023
2 parents 899d7a7 + e7f8ee8 commit f909b1e
Show file tree
Hide file tree
Showing 25 changed files with 342 additions and 148 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,7 @@ COPY --from=build /app/_build/prod/rel/livebook /app
# Make release files available to any user, in case someone
# runs the container with `--user`
RUN chmod -R go=u /app
# Make all home files available (specifically .mix/)
RUN chmod -R go=u $HOME

CMD [ "/app/bin/livebook", "start" ]
55 changes: 35 additions & 20 deletions assets/css/js_interop.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ solely client-side operations.
[data-el-section-headline]:not(:hover):not([data-js-focused])
[data-el-section-collapse-button],
[data-el-section]:not([data-js-collapsed]) [data-el-section-expand-button],
[data-el-session]:not([data-js-code-zen])
[data-el-session]:not([data-js-view="code-zen"])
[data-el-section][data-js-collapsed]
> [data-el-section-content],
[data-el-section]:not([data-js-collapsed])
Expand Down Expand Up @@ -276,40 +276,55 @@ solely client-side operations.
@apply hidden;
}

[data-el-session][data-js-code-zen] [data-el-section-headline],
[data-el-session][data-js-code-zen] [data-el-section-subheadline],
[data-el-session][data-js-code-zen] [data-el-section-subheadline-collapsed],
[data-el-session][data-js-code-zen] [data-el-cell][data-type="markdown"],
[data-el-session][data-js-code-zen] [data-el-actions],
[data-el-session][data-js-code-zen] [data-el-insert-buttons] {
/* === Views === */

[data-el-session][data-js-view="code-zen"] [data-el-section-headline],
[data-el-session][data-js-view="code-zen"] [data-el-section-subheadline],
[data-el-session][data-js-view="code-zen"]
[data-el-section-subheadline-collapsed],
[data-el-session][data-js-view="code-zen"] [data-el-cell][data-type="markdown"],
[data-el-session][data-js-view="code-zen"] [data-el-actions],
[data-el-session][data-js-view="code-zen"] [data-el-insert-buttons] {
@apply hidden;
}

[data-el-session][data-js-code-zen] [data-el-sections-container] {
[data-el-session][data-js-view="code-zen"] [data-el-sections-container] {
@apply space-y-0 mt-0;
}

[data-el-session][data-js-code-zen][data-js-no-outputs]
[data-el-outputs-container] {
@apply hidden;
[data-el-session][data-js-view="code-zen"] [data-el-view-toggle="code-zen"] {
@apply text-green-bright-400;
}

[data-el-session][data-js-code-zen][data-js-no-outputs]
[data-el-code-zen-outputs-toggle]
[data-label-hide] {
@apply hidden;
[data-el-session][data-js-view="presentation"]
[data-el-section-headline]:not([data-js-focused]),
[data-el-session][data-js-view="presentation"]
[data-el-section-subheadline]:not([data-js-focused]),
[data-el-session][data-js-view="presentation"]
[data-el-cell]:not([data-js-focused]),
[data-el-session][data-js-view="presentation"]
[data-el-js-view-iframes]
iframe:not([data-js-focused]) {
@apply opacity-10;
}

[data-el-session][data-js-code-zen]:not([data-js-no-outputs])
[data-el-code-zen-outputs-toggle]
[data-label-show] {
[data-el-session][data-js-view="presentation"] [data-el-sidebar],
[data-el-session][data-js-view="presentation"] [data-el-side-panel],
[data-el-session][data-js-view="presentation"] [data-el-toggle-sidebar] {
@apply hidden;
}

[data-el-session][data-js-code-zen] [data-el-code-zen-enable] {
[data-el-session][data-js-view="presentation"]
[data-el-view-toggle="presentation"] {
@apply text-green-bright-400;
}

[data-el-session]:is([data-js-view="code-zen"], [data-js-view="presentation"])
[data-el-views-disabled] {
@apply hidden;
}

[data-el-session]:not([data-js-code-zen]) [data-el-focus-mode-options] {
[data-el-session]:not([data-js-view="code-zen"], [data-js-view="presentation"])
[data-el-views-enabled] {
@apply hidden;
}
103 changes: 66 additions & 37 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { loadUserData } from "./lib/user";
import { loadAppAuthToken } from "./lib/app";
import { settingsStore } from "./lib/settings";
import { registerTopbar, registerGlobalEventHandlers } from "./events";
import { cookieOptions } from "./lib/utils";

function connect() {
const csrfToken = document
Expand Down Expand Up @@ -62,44 +63,72 @@ function connect() {
}

// When Livebook runs in a cross-origin iframe the browser may restrict access
// to cookies. This is the case in Safari with the "Prevent cross-site tracking"
// option enabled, which is the default. Without cookies access, the session
// is not stored, so CSRF tokens are invalid. Consequently, LV keeps reloading
// the page, as we try to connect the socket with invalid token. To work around
// this we tell the user to open Livebook outside the iframe.

if (document.hasStorageAccess) {
document.hasStorageAccess().then((hasStorageAccess) => {
if (hasStorageAccess) {
connect();
} else {
const overlayEl = document.createElement("div");

overlayEl.innerHTML = `
<div class="fixed top-0 bottom-0 left-0 right-0 z-[1000] px-4 py-8 bg-gray-900/95 flex justify-center items-center">
<div class="max-w-[600px] w-full flex flex-col">
<div class="text-xl text-gray-100 font-medium">
Action required
</div>
<div class="mt-3 text-sm text-gray-300">
It looks like Livebook does not have access to cookies. This usually happens when
it runs in an iframe. To make sure the app is fully functional open it in a new
tab directly.
</div>
<div class="mt-6">
<a id="open-app" class="button-base button-blue" target="_blank">
Open app
</a>
</div>
</div>
// to cookies. Without cookies access, the session is not stored, so CSRF tokens
// are invalid. Consequently, LV keeps reloading the page, as we try to connect
// the socket with invalid token. To work around this we tell the user to open
// Livebook outside the iframe.
//
// The behaviour varies across browsers and browsing modes (regular and private).
// A few examples (at the time of writing):
//
// * Safari by default blocks all cross-origin cookies. This is controlled by
// the "Prevent cross-site tracking" option
//
// * Chrome in incognito mode blocks all cross-origin cookies, can be relaxed
// on per-site basis
//
// * Firefox implements state partitioning (1) and it is enabled for storage
// by default since Firefox 103 (2). With storage partitioning, the embedded
// site gets a separate storage bucket scoped by the top-level origin, so
// the site generally works as expected
//
// * Brave also implements storage partitioning (3)
//
// To detect whether cookies are allowed, we check if we can programmatically
// set a cookie.
//
// Also see the proposal (4), which may streamline this in the future.
//
// (1): https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning#state_partitioning
// (2): https://www.mozilla.org/en-US/firefox/103.0/releasenotes
// (3): https://brave.com/privacy-updates/7-ephemeral-storage
// (4): https://github.com/privacycg/CHIPS

if (hasCookiesAccess()) {
connect();
} else {
const overlayEl = document.createElement("div");

overlayEl.innerHTML = `
<div class="fixed top-0 bottom-0 left-0 right-0 z-[1000] px-4 py-8 bg-gray-900/95 flex justify-center items-center">
<div class="max-w-[600px] w-full flex flex-col">
<div class="text-xl text-gray-100 font-medium">
Action required
</div>
`;
<div class="mt-3 text-sm text-gray-300">
It looks like Livebook does not have access to cookies. This usually happens when
it runs in an iframe. To make sure the app is fully functional open it in a new
tab directly. Alternatively you can relax security settings for this page to allow
third-party cookies.
</div>
<div class="mt-6">
<a id="open-app" class="button-base button-blue" target="_blank">
Open app
</a>
</div>
</div>
</div>
`;

overlayEl.querySelector("#open-app").href = window.location;
overlayEl.querySelector("#open-app").href = window.location;

document.body.appendChild(overlayEl);
}
});
} else {
connect();
document.body.appendChild(overlayEl);
}

function hasCookiesAccess() {
document.cookie = `lb:probe_cookie=;path=/${cookieOptions()}`;

return document.cookie
.split("; ")
.some((cookie) => cookie.startsWith(`lb:probe_cookie=`));
}
23 changes: 23 additions & 0 deletions assets/js/hooks/js_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ const JSView = {
// default timeout of 10s, so we increase it
30_000
);

this.unsubscribeFromCellEvents = globalPubSub.subscribe(
"navigation",
(event) => this.handleNavigationEvent(event)
);
},

updated() {
Expand All @@ -167,6 +172,7 @@ const JSView = {
this.channel.push("disconnect", { ref: this.props.ref });

this.unsubscribeFromJSViewEvents();
this.unsubscribeFromCellEvents();
},

getProps() {
Expand Down Expand Up @@ -454,6 +460,23 @@ const JSView = {
});
}
},

handleNavigationEvent(event) {
if (event.type === "element_focused") {
// If a parent focusable element is focused, mirror the attribute
// to the iframe element. This way if we need to apply style rules
// (such as opacity) to focused elements, we can target the iframe
// elements placed elsewhere in the DOM

const focusableEl = this.el.closest(`[data-focusable-id]`);
const focusableId = focusableEl ? focusableEl.dataset.focusableId : null;

this.iframe.toggleAttribute(
"data-js-focused",
focusableId === event.focusableId
);
}
},
};

export default JSView;
67 changes: 37 additions & 30 deletions assets/js/hooks/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const Session = {

this.focusedId = null;
this.insertMode = false;
this.codeZen = false;
this.view = null;
this.keyBuffer = new KeyBuffer();
this.clientsMap = {};
this.lastLocationReportByClientId = {};
Expand Down Expand Up @@ -131,20 +131,9 @@ const Session = {
this.handleCellIndicatorsClick(event)
);

this.getElement("code-zen-enable-button").addEventListener(
"click",
(event) => this.setCodeZen(true)
);

this.getElement("code-zen-disable-button").addEventListener(
"click",
(event) => this.setCodeZen(false)
);

this.getElement("code-zen-outputs-toggle").addEventListener(
"click",
(event) => this.el.toggleAttribute("data-js-no-outputs")
);
this.getElement("views").addEventListener("click", (event) => {
this.handleViewsClick(event);
});

this.getElement("section-toggle-collapse-all-button").addEventListener(
"click",
Expand Down Expand Up @@ -421,15 +410,17 @@ const Session = {
} else if (keyBuffer.tryMatch(["N"])) {
this.insertCellAboveFocused("code");
} else if (keyBuffer.tryMatch(["m"])) {
!this.codeZen && this.insertCellBelowFocused("markdown");
!this.isViewCodeZen() && this.insertCellBelowFocused("markdown");
} else if (keyBuffer.tryMatch(["M"])) {
!this.codeZen && this.insertCellAboveFocused("markdown");
} else if (keyBuffer.tryMatch(["z"])) {
this.setCodeZen(!this.codeZen);
!this.isViewCodeZen() && this.insertCellAboveFocused("markdown");
} else if (keyBuffer.tryMatch(["v", "z"])) {
this.toggleView("code-zen");
} else if (keyBuffer.tryMatch(["v", "p"])) {
this.toggleView("presentation");
} else if (keyBuffer.tryMatch(["c"])) {
!this.codeZen && this.toggleCollapseSection();
!this.isViewCodeZen() && this.toggleCollapseSection();
} else if (keyBuffer.tryMatch(["C"])) {
!this.codeZen && this.toggleCollapseAllSections();
!this.isViewCodeZen() && this.toggleCollapseAllSections();
}
}
},
Expand Down Expand Up @@ -975,18 +966,27 @@ const Session = {
});
},

setCodeZen(enabled) {
this.codeZen = enabled;
handleViewsClick(event) {
const button = event.target.closest(`[data-el-view-toggle]`);

// If nothing is focused, use the first cell in the viewport
const focusedId = this.focusedId || this.nearbyFocusableId(null, 0);
if (button) {
const view = button.getAttribute("data-el-view-toggle");
this.toggleView(view);
}
},

if (enabled) {
this.el.setAttribute("data-js-code-zen", "");
toggleView(view) {
if (this.view === view) {
this.view = null;
this.el.removeAttribute("data-js-view");
} else {
this.el.removeAttribute("data-js-code-zen");
this.view = view;
this.el.setAttribute("data-js-view", view);
}

// If nothing is focused, use the first cell in the viewport
const focusedId = this.focusedId || this.nearbyFocusableId(null, 0);

if (focusedId) {
const visibleId = this.ensureVisibleFocusableEl(focusedId);

Expand Down Expand Up @@ -1034,7 +1034,6 @@ const Session = {
}
}
},

// Server event handlers

handleCellInserted(cellId) {
Expand All @@ -1046,7 +1045,7 @@ const Session = {

handleCellDeleted(cellId, siblingCellId) {
if (this.focusedId === cellId) {
if (this.codeZen) {
if (this.isViewCodeZen()) {
const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId);
this.setFocusedEl(visibleSiblingId);
} else {
Expand Down Expand Up @@ -1333,6 +1332,14 @@ const Session = {
getElement(name) {
return this.el.querySelector(`[data-el-${name}]`);
},

isViewCodeZen() {
return this.view === "code-zen";
},

isViewPresentation() {
return this.view === "presentation";
},
};

/**
Expand Down
4 changes: 2 additions & 2 deletions assets/js/lib/user.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decodeBase64, encodeBase64 } from "./utils";
import { cookieOptions, decodeBase64, encodeBase64 } from "./utils";

const USER_DATA_COOKIE = "lb:user_data";

Expand Down Expand Up @@ -38,6 +38,6 @@ function getCookieValue(key) {
}

function setCookie(key, value, maxAge) {
const cookie = `${key}=${value};max-age=${maxAge};path=/`;
const cookie = `${key}=${value};max-age=${maxAge};path=/${cookieOptions()}`;
document.cookie = cookie;
}
Loading

0 comments on commit f909b1e

Please sign in to comment.