Skip to content

Commit 7af822c

Browse files
committed
UI: autodetect pasted URL
1 parent 819bba8 commit 7af822c

File tree

6 files changed

+128
-10
lines changed

6 files changed

+128
-10
lines changed

binderhub/build.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -455,15 +455,10 @@ def get_builder_volumes(self):
455455

456456
return volumes, volume_mounts
457457

458-
def submit(self):
458+
def get_env(self):
459459
"""
460-
Submit a build pod to create the image for the repository.
461-
462-
Progress of the build can be monitored by listening for items in
463-
the Queue passed to the constructor as `q`.
460+
Get the Kubernetes environment variables for the build pod.
464461
"""
465-
volumes, volume_mounts = self.get_builder_volumes()
466-
467462
env = [
468463
client.V1EnvVar(name=key, value=value)
469464
for key, value in self.extra_envs.items()
@@ -472,16 +467,23 @@ def submit(self):
472467
env.append(
473468
client.V1EnvVar(name="GIT_CREDENTIAL_ENV", value=self.git_credentials)
474469
)
475-
476470
if self.registry_credentials:
477471
env.append(
478472
client.V1EnvVar(
479473
name="CONTAINER_ENGINE_REGISTRY_CREDENTIALS",
480474
value=json.dumps(self.registry_credentials),
481475
)
482476
)
477+
return env
478+
479+
def make_pod(self):
480+
"""
481+
Make a Kubernetes pod specification for the build pod.
482+
"""
483+
volumes, volume_mounts = self.get_builder_volumes()
484+
env = self.get_env()
483485

484-
self.pod = client.V1Pod(
486+
pod = client.V1Pod(
485487
metadata=client.V1ObjectMeta(
486488
name=self.name,
487489
labels={
@@ -528,6 +530,16 @@ def submit(self):
528530
affinity=self.get_affinity(),
529531
),
530532
)
533+
return pod
534+
535+
def submit(self):
536+
"""
537+
Submit a build pod to create the image for the repository.
538+
539+
Progress of the build can be monitored by listening for items in
540+
the Queue passed to the constructor as `q`.
541+
"""
542+
self.pod = self.make_pod()
531543

532544
try:
533545
_ = self.api.create_namespaced_pod(

binderhub/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ def generate_config(self):
1010
"repo_providers"
1111
].items():
1212
config[repo_provider_class_alias] = repo_provider_class.labels
13+
config[repo_provider_class_alias][
14+
"display_name"
15+
] = repo_provider_class.display_name
16+
config[repo_provider_class_alias][
17+
"regex_detect"
18+
] = repo_provider_class.regex_detect
1319
return config
1420

1521
async def get(self):

binderhub/repoproviders.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ class RepoProvider(LoggingConfigurable):
9999
config=True,
100100
)
101101

102+
regex_detect = List(
103+
[],
104+
help="""
105+
List of JavaScript regexes to detect repository parameters from URLs.
106+
Regexes must contain named groups for `repo`, and optional named groups
107+
for `ref`, `filepath`, `urlpath`.
108+
109+
Note named groups in Javascript are not the same as in Python.
110+
""",
111+
)
112+
102113
unresolved_ref = Unicode()
103114

104115
git_credentials = Unicode(
@@ -192,6 +203,15 @@ def is_valid_sha1(sha1):
192203
class FakeProvider(RepoProvider):
193204
"""Fake provider for local testing of the UI"""
194205

206+
name = Unicode("Fake")
207+
208+
display_name = "Fake GitHub"
209+
210+
regex_detect = [
211+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
212+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
213+
]
214+
195215
labels = {
196216
"text": "Fake Provider",
197217
"tag_text": "Fake Ref",
@@ -627,6 +647,13 @@ def _default_git_credentials(self):
627647
return rf"username=binderhub\npassword={self.private_token}"
628648
return ""
629649

650+
# Gitlab repos can be nested under projects
651+
_regex_detect_base = r"^https://gitlab\.com/(?<repo>[^/]+/[^/]+(/[^/-][^/]+)*)"
652+
regex_detect = [
653+
_regex_detect_base + r"(/-/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
654+
_regex_detect_base + r"(/-/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
655+
]
656+
630657
labels = {
631658
"text": "GitLab.com repository or URL",
632659
"tag_text": "Git ref (branch, tag, or commit)",
@@ -780,6 +807,11 @@ def _default_git_credentials(self):
780807
return rf"username={self.access_token}\npassword=x-oauth-basic"
781808
return ""
782809

810+
regex_detect = [
811+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/blob/(?<ref>[^/]+)(/(?<filepath>.+))?)?$",
812+
r"^https://github\.com/(?<repo>[^/]+/[^/]+)(/tree/(?<ref>[^/]+)(/(?<urlpath>.+))?)?$",
813+
]
814+
783815
labels = {
784816
"text": "GitHub repository name or URL",
785817
"tag_text": "Git ref (branch, tag, or commit)",
@@ -973,6 +1005,10 @@ class GistRepoProvider(GitHubRepoProvider):
9731005
help="Flag for allowing usages of secret Gists. The default behavior is to disallow secret gists.",
9741006
)
9751007

1008+
regex_detect = [
1009+
r"^https://gist\.github\.com/(?<repo>[^/]+/[^/]+)(/(?<ref>[^/]+))?$"
1010+
]
1011+
9761012
labels = {
9771013
"text": "Gist ID (username/gistId) or URL",
9781014
"tag_text": "Git commit SHA",

binderhub/static/js/index.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BinderRepository } from "@jupyterhub/binderhub-client";
77
import { updatePathText } from "./src/path";
88
import { nextHelpText } from "./src/loading";
99
import { updateFavicon } from "./src/favicon";
10+
import { detect } from "./src/autodetect";
1011

1112
import "xterm/css/xterm.css";
1213

@@ -166,7 +167,39 @@ function indexMain() {
166167
updatePathText();
167168
updateRepoText(BASE_URL);
168169

169-
$("#repository").on("keyup paste change", function () {
170+
// If the user pastes a URL into the repository field try to autodetect
171+
// In all other cases don't do anything to avoid overwriting the user's input
172+
$("#repository").on("paste", function (ev) {
173+
const event = ev.originalEvent;
174+
const pastedText = (event.clipboardData || window.clipboardData)
175+
?.getData("text")
176+
.trim();
177+
if (pastedText) {
178+
const fields = detect(pastedText);
179+
console.log(fields);
180+
if (fields) {
181+
$("#provider_prefix-selected").text(fields.display_name);
182+
$("#provider_prefix").val(fields.provider);
183+
$("#repository").val(fields.repo);
184+
if (fields.ref) {
185+
$("#ref").val(fields.ref);
186+
}
187+
if (fields.path) {
188+
$("#filepath").val(fields.path);
189+
$("#url-or-file-selected").text(
190+
fields.pathType === "filepath" ? "File" : "URL",
191+
);
192+
}
193+
updatePathText();
194+
event.preventDefault();
195+
updateRepoText(BASE_URL);
196+
}
197+
}
198+
// else autodetect failed, let the paste go through
199+
updateUrls(BADGE_BASE_URL);
200+
});
201+
202+
$("#repository").on("keyup change", function () {
170203
updateUrls(BADGE_BASE_URL);
171204
});
172205

binderhub/static/js/src/autodetect.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { getConfigDict } from "./repo";
2+
3+
export function detect(text) {
4+
// Assumes configDict was already loaded by another module
5+
const configDict = getConfigDict();
6+
for (const provider in configDict) {
7+
const regex_detect = configDict[provider].regex_detect || [];
8+
for (const regex of regex_detect) {
9+
const m = text.match(regex);
10+
if (m?.groups.repo) {
11+
return {
12+
provider: provider,
13+
display_name: configDict[provider].display_name,
14+
repo: m.groups.repo,
15+
ref: m.groups.ref,
16+
path: m.groups.filepath || m.groups.urlpath || null,
17+
pathType: m.groups.filepath
18+
? "filepath"
19+
: m.groups.urlpath
20+
? "urlpath"
21+
: null,
22+
};
23+
}
24+
}
25+
}
26+
return null;
27+
}

binderhub/static/js/src/repo.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ export function updateRepoText(baseUrl) {
3535
setLabels();
3636
}
3737
}
38+
39+
export function getConfigDict() {
40+
return configDict;
41+
}

0 commit comments

Comments
 (0)