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

Rewrite the frontend #1856

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31910bc
Rewrite the frontend completely
yuvipanda May 15, 2024
5c1183f
Split homepage into its own file
yuvipanda May 17, 2024
2e2a3ea
Add a loading page
yuvipanda May 18, 2024
55e0a41
Add nbviewer support
yuvipanda May 18, 2024
bb480e8
Re-implement the loader animation & text
yuvipanda May 18, 2024
f63e9b9
Update favicon correctly based on phases
yuvipanda May 18, 2024
ea7dbe7
Remove and cleanup unused stuff
yuvipanda May 18, 2024
abbf864
Add more type annotations
yuvipanda May 18, 2024
5661f17
Fix a few more typescript detected errors
yuvipanda May 18, 2024
b02256e
Don't enforce strict type checks yet
yuvipanda May 18, 2024
ffb3884
Create a central spec class
yuvipanda May 18, 2024
d6b549f
Document & rename spec class to be better
yuvipanda May 23, 2024
5da4111
Pass launchSpec correctly
yuvipanda May 23, 2024
23e491e
Remove GA code, add back extra_footer_scripts
yuvipanda May 23, 2024
b5c1c38
Move redirect calculation to Spec object
yuvipanda May 24, 2024
1bc530a
Remove unused static font
yuvipanda May 24, 2024
e4becd7
Move about page to frontend rendering
yuvipanda May 24, 2024
5a6adac
Fix case of component file
yuvipanda May 29, 2024
354118a
Remove remaining reference to `_config`
yuvipanda May 29, 2024
484680b
Support setting banner message
yuvipanda Jul 1, 2024
3e31936
Merge remote-tracking branch 'upstream/main' into closure
yuvipanda Jul 12, 2024
0ad9347
Remove unused functionality from binderhub-client
yuvipanda Jul 12, 2024
ea9d565
Add functionality to view logs in raw form
yuvipanda Jul 31, 2024
2c9d89f
Kill some unnecessary useEffects
yuvipanda Aug 1, 2024
7c55383
Add badge generator
yuvipanda Aug 2, 2024
c65214d
Merge remote-tracking branch 'upstream/main' into closure
yuvipanda Aug 2, 2024
c46a691
Add OpenGraph social card support
yuvipanda Aug 3, 2024
f942938
Fix copy buttons
yuvipanda Aug 3, 2024
ab59cb1
Tweak style of badge generator a little
yuvipanda Aug 3, 2024
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
46 changes: 36 additions & 10 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
module.exports = {
env: {
browser: true,
jquery: true,
node: true,
es6: true,
"jest/globals": true,
},
extends: ["eslint:recommended"],
ignorePatterns: ["**/dist"],
parser: "@babel/eslint-parser",
plugins: ["jest"],
rules: {},
es2021: true,
},
extends: ["eslint:recommended", "plugin:react/recommended"],
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["react"],
rules: {
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off",
// Temporarily turn off prop-types
"react/prop-types": "off",
"no-unused-vars": ["error", { args: "after-used" }],
},
ignorePatterns: [
"jupyterhub_fancy_profiles/static/*.js",
"webpack.config.js",
"babel.config.js",
],
settings: {
react: {
version: "detect",
},
},
};
5 changes: 4 additions & 1 deletion babel.config.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"presets": ["@babel/preset-env"]
"presets": [
"@babel/preset-env",
["@babel/preset-react", { "runtime": "automatic" }]
]
}
45 changes: 34 additions & 11 deletions binderhub/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@
)
from traitlets.config import Application

from .base import AboutHandler, Custom404, VersionHandler
from .base import Custom404, VersionHandler
from .build import BuildExecutor, KubernetesBuildExecutor, KubernetesCleaner
from .builder import BuildHandler
from .config import ConfigHandler
from .events import EventLog
from .handlers.repoproviders import RepoProvidersHandlers
from .health import HealthHandler, KubernetesHealthHandler
from .launcher import Launcher
from .log import log_request
from .main import LegacyRedirectHandler, MainHandler, ParameterizedMainHandler
from .main import LegacyRedirectHandler, RepoLaunchUIHandler, UIHandler
from .metrics import MetricsHandler
from .quota import KubernetesLaunchQuota, LaunchQuota
from .ratelimit import RateLimiter
Expand Down Expand Up @@ -107,6 +107,11 @@ def _log_level(self):
None,
allow_none=True,
help="""
..deprecated::

No longer supported. If you want to use Google Analytics, use :attr:`extra_footer_scripts`
to load JS from Google Analytics.

The Google Analytics code to use on the main page.

Note that we'll respect Do Not Track settings, despite the fact that GA does not.
Expand All @@ -118,6 +123,11 @@ def _log_level(self):
google_analytics_domain = Unicode(
"auto",
help="""
..deprecated::

No longer supported. If you want to use Google Analytics, use :attr:`extra_footer_scripts`
to load JS from Google Analytics.

The Google Analytics domain to use on the main page.

By default this is set to 'auto', which sets it up for current domain and all
Expand All @@ -126,6 +136,13 @@ def _log_level(self):
config=True,
)

@observe("google_analytics_domain", "google_analytics_code")
def _google_analytics_deprecation(self, change):
if change.new:
raise ValueError(
f"Setting {change.owner.__class__.__name__}.{change.name} is no longer supported. Use {change.owner.__class__.__name__}.extra_footer_scripts to load Google Analytics JS directly"
)

about_message = Unicode(
"",
help="""
Expand Down Expand Up @@ -798,7 +815,6 @@ def _template_path_default(self):
- /versions
- /build/([^/]+)/(.+)
- /health
- /_config
- /* -> shows a 404 page
""",
config=True,
Expand Down Expand Up @@ -945,8 +961,6 @@ def initialize(self, *args, **kwargs):
"registry": registry,
"traitlets_config": self.config,
"traitlets_parent": self,
"google_analytics_code": self.google_analytics_code,
"google_analytics_domain": self.google_analytics_domain,
"about_message": self.about_message,
"banner_message": self.banner_message,
"extra_footer_scripts": self.extra_footer_scripts,
Expand Down Expand Up @@ -975,15 +989,23 @@ def initialize(self, *args, **kwargs):
(r"/versions", VersionHandler),
(r"/build/([^/]+)/(.+)", BuildHandler),
(r"/health", self.health_handler_class, {"hub_url": self.hub_url_local}),
(r"/_config", ConfigHandler),
(r"/api/repoproviders", RepoProvidersHandlers),
]
if not self.enable_api_only_mode:
# In API only mode the endpoints in the list below
# are unregistered as they don't make sense in a API only scenario
# are not registered since they are primarily about providing UI

for provider_id in self.repo_providers:
# Register launchable URLs for all our repo providers
# These render social previews, but otherwise redirect to UIHandler
handlers += [
(
rf"/v2/({provider_id})/(.+)",
RepoLaunchUIHandler,
{"repo_provider": self.repo_providers[provider_id]},
)
]
handlers += [
(r"/about", AboutHandler),
(r"/v2/([^/]+)/(.+)", ParameterizedMainHandler),
(r"/", MainHandler),
(r"/repo/([^/]+)/([^/]+)(/.*)?", LegacyRedirectHandler),
# for backward-compatible mybinder.org badge URLs
# /assets/images/badge.svg
Expand Down Expand Up @@ -1050,6 +1072,7 @@ def initialize(self, *args, **kwargs):
)
},
),
(r"/.*", UIHandler),
]
# This needs to be the last handler in the list, because it needs to match "everything else"
handlers.append((r".*", Custom404))
Expand Down
16 changes: 0 additions & 16 deletions binderhub/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,6 @@ def prepare(self):
raise web.HTTPError(404)


class AboutHandler(BaseHandler):
"""Serve the about page"""

async def get(self):
self.render_template(
"about.html",
base_url=self.settings["base_url"],
submit=False,
binder_version=binder_version,
message=self.settings["about_message"],
google_analytics_code=self.settings["google_analytics_code"],
google_analytics_domain=self.settings["google_analytics_domain"],
extra_footer_scripts=self.settings["extra_footer_scripts"],
)


class VersionHandler(BaseHandler):
"""Serve information about versions running"""

Expand Down
Empty file added binderhub/handlers/__init__.py
Empty file.
15 changes: 15 additions & 0 deletions binderhub/handlers/repoproviders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import json

from ..base import BaseHandler


class RepoProvidersHandlers(BaseHandler):
"""Serve config"""

async def get(self):
config = [
repo_provider_class.display_config
for repo_provider_class in self.settings["repo_providers"].values()
]
self.set_header("Content-type", "application/json")
self.write(json.dumps(config))
146 changes: 41 additions & 105 deletions binderhub/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,66 @@
Main handler classes for requests
"""

import time
import urllib.parse

import jwt
from tornado.httpclient import AsyncHTTPClient, HTTPRequest
from tornado.httputil import url_concat
from tornado.log import app_log
from tornado.web import HTTPError, authenticated
from tornado.web import authenticated

from . import __version__ as binder_version
from .base import BaseHandler

SPEC_NAMES = {
"gh": "GitHub",
"gist": "Gist",
"gl": "GitLab",
"git": "Git repo",
"zenodo": "Zenodo",
"figshare": "Figshare",
"hydroshare": "Hydroshare",
"dataverse": "Dataverse",
"ckan": "CKAN",
}

class UIHandler(BaseHandler):
"""
Responds to most UI Page Requests
"""

class MainHandler(BaseHandler):
"""Main handler for requests"""
def initialize(self):
self.opengraph_title = "The Binder Project"
return super().initialize()

@authenticated
def get(self):
repoproviders_display_config = [
repo_provider_class.display_config
for repo_provider_class in self.settings["repo_providers"].values()
]
page_config = {
"baseUrl": self.settings["base_url"],
"badgeBaseUrl": self.get_badge_base_url(),
"logoUrl": self.static_url("logo.svg"),
"logoWidth": "320px",
"repoProviders": repoproviders_display_config,
"aboutMessage": self.settings["about_message"],
"bannerHtml": self.settings["banner_message"],
"binderVersion": binder_version,
}
self.render_template(
"index.html",
badge_base_url=self.get_badge_base_url(),
base_url=self.settings["base_url"],
submit=False,
google_analytics_code=self.settings["google_analytics_code"],
google_analytics_domain=self.settings["google_analytics_domain"],
"page.html",
page_config=page_config,
extra_footer_scripts=self.settings["extra_footer_scripts"],
repo_providers=self.settings["repo_providers"],
opengraph_title=self.opengraph_title,
)


class ParameterizedMainHandler(BaseHandler):
"""Main handler that allows different parameter settings"""

@authenticated
async def get(self, provider_prefix, _unescaped_spec):
prefix = "/v2/" + provider_prefix
spec = self.get_spec_from_request(prefix)
spec = spec.rstrip("/")
try:
self.get_provider(provider_prefix, spec=spec)
except HTTPError:
raise
except Exception as e:
app_log.error(
"Failed to construct provider for %s/%s",
provider_prefix,
spec,
)
# FIXME: 400 assumes it's the user's fault (?)
# maybe we should catch a special InvalidSpecError here
raise HTTPError(400, str(e))

provider_spec = f"{provider_prefix}/{spec}"
social_desc = f"{SPEC_NAMES[provider_prefix]}: {spec}"
nbviewer_url = None
if provider_prefix == "gh":
# We can only produce an nbviewer URL for github right now
nbviewer_url = "https://nbviewer.jupyter.org/github"
org, repo_name, ref = spec.split("/", 2)
# NOTE: tornado unquotes query arguments too -> notebooks%2Findex.ipynb becomes notebooks/index.ipynb
filepath = self.get_argument("labpath", "").lstrip("/")
if not filepath:
filepath = self.get_argument("filepath", "").lstrip("/")

# Check the urlpath parameter for a file path, if so use it for the filepath
urlpath = self.get_argument("urlpath", "").lstrip("/")
if urlpath and "/tree/" in urlpath:
filepath = urlpath.split("tree/", 1)[-1]
class RepoLaunchUIHandler(UIHandler):
"""
Responds to /v2/ launch URLs only

blob_or_tree = "blob" if filepath else "tree"
nbviewer_url = (
f"{nbviewer_url}/{org}/{repo_name}/{blob_or_tree}/{ref}/{filepath}"
)
Forwards to UIHandler, but puts out an opengraph_title for social previews
"""

# Check if the nbviewer URL is valid and would display something
# useful to the reader, if not we don't show it
client = AsyncHTTPClient()
# quote any unicode characters in the URL
proto, rest = nbviewer_url.split("://")
rest = urllib.parse.quote(rest)
def initialize(self, repo_provider):
self.repo_provider = repo_provider
return super().initialize()

request = HTTPRequest(
proto + "://" + rest,
method="HEAD",
user_agent="BinderHub",
)
response = await client.fetch(request, raise_error=False)
if response.code >= 400:
nbviewer_url = None
@authenticated
def get(self, provider_id, _escaped_spec):
prefix = "/v2/" + provider_id
spec = self.get_spec_from_request(prefix).rstrip("/")

build_token = jwt.encode(
{
"exp": int(time.time()) + self.settings["build_token_expires_seconds"],
"aud": provider_spec,
"origin": self.token_origin(),
},
key=self.settings["build_token_secret"],
algorithm="HS256",
)
self.render_template(
"loading.html",
base_url=self.settings["base_url"],
badge_base_url=self.get_badge_base_url(),
build_token=build_token,
provider_spec=provider_spec,
social_desc=social_desc,
nbviewer_url=nbviewer_url,
# urlpath=self.get_argument('urlpath', None),
submit=True,
google_analytics_code=self.settings["google_analytics_code"],
google_analytics_domain=self.settings["google_analytics_domain"],
extra_footer_scripts=self.settings["extra_footer_scripts"],
self.opengraph_title = (
f"{self.repo_provider.display_config['displayName']}: {spec}"
)
return super().get()


class LegacyRedirectHandler(BaseHandler):
Expand Down
Loading
Loading