add and run djlint formatter

This commit is contained in:
Min RK
2024-04-18 17:49:07 +02:00
parent fb1614e20a
commit d9ce1b917f
20 changed files with 740 additions and 842 deletions

View File

@@ -33,6 +33,15 @@ repos:
rev: v4.0.0-alpha.8
hooks:
- id: prettier
exclude: .*/templates/.*
# autoformat HTML templates
- repo: https://github.com/djlint/djLint
rev: v1.34.1
hooks:
- id: djlint-reformat-jinja
files: ".*templates/.*.html"
types_or: ["html"]
# Autoformat and linting, misc. details
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@@ -1,26 +1,24 @@
{%- extends "!layout.html" %}
{# not sure why, but theme CSS prevents scrolling within redoc content
# If this were fixed, we could keep the navbar and footer
#}
{%- block css %}{%- endblock %}
{%- block docs_navbar %}{%- endblock %}
{%- block footer %}{% endblock %}
{%- block body_tag %}
<body>
{%- endblock %}
{% block css %}
{% endblock css %}
{% block docs_navbar %}
{% endblock docs_navbar %}
{% block footer %}
{% endblock footer %}
{# djlint: off #}
{%- block body_tag -%}<body>{%- endblock body_tag %}
{# djlint: on #}
{%- block extrahead %}
{{ super() }}
<link href="{{ pathto('_static/redoc-fonts.css', 1) }}"
rel="stylesheet"
/>
<script src="{{ pathto('_static/redoc.js', 1) }}"></script>
{%- endblock %}
{{ super() }}
<link href="{{ pathto('_static/redoc-fonts.css', 1) }}" rel="stylesheet" />
<script src="{{ pathto('_static/redoc.js', 1) }}"></script>
{%- endblock extrahead %}
{%- block content %}
<redoc id="redoc-spec"></redoc>
<script>
<redoc id="redoc-spec"></redoc>
<script>
if (location.protocol === "file:") {
document.body.innerText = "Rendered API specification doesn't work with file: protocol. Use sphinx-autobuild to do local builds of the docs, served over HTTP."
} else {
@@ -30,5 +28,5 @@
document.getElementById("redoc-spec"),
);
}
</script>
{%- endblock %}
</script>
{%- endblock content %}

View File

@@ -1,9 +1,10 @@
{% extends "templates/page.html" %} {% block announcement %}
<div class="container text-center announcement"></div>
{% endblock %} {% block script %} {{ super() }}
<script>
{% extends "templates/page.html" %}
{% block announcement %}<div class="container text-center announcement"></div>{% endblock %}
{% block script %}
{{ super() }}
<script>
$.get("/services/announcement/", function (data) {
$(".announcement").html(data["announcement"]);
});
</script>
</script>
{% endblock %}

View File

@@ -2,11 +2,9 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JupyterHub</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/css/style.min.css" type="text/css" />
</head>
<body>

View File

@@ -7,30 +7,23 @@ It makes the following modifications:
- update logo url to jupyterhub
- remove `?redirects` url param that may be added by jupyterhub
#}
{% extends "templates/page.html" %}
{% block header_buttons %}
{{ super() }}
<span>
<a
href="{{hub_control_panel_url}}"
id="jupyterhub-control-panel-link"
class="btn btn-default btn-sm navbar-btn pull-right"
style="margin-right: 4px; margin-left: 2px"
>
Control Panel
</a>
</span>
{{ super() }}
<span>
<a href="{{ hub_control_panel_url }}"
id="jupyterhub-control-panel-link"
class="btn btn-default btn-sm navbar-btn pull-right"
style="margin-right: 4px;
margin-left: 2px">Control Panel</a>
</span>
{% endblock %}
{% block logo %}
<img src="{{logo_url}}" alt="Jupyter Notebook" />
<img src="{{ logo_url }}" alt="Jupyter Notebook" />
{% endblock logo %}
{% block script %}
{{ super() }}
<script type="text/javascript">
{{ super() }}
<script type="text/javascript">
function _remove_redirects_param() {
// remove ?redirects= param from URL so that
// successful page loads don't increment the redirect loop counter
@@ -57,5 +50,5 @@ It makes the following modifications:
}
}
_remove_redirects_param();
</script>
</script>
{% endblock script %}

View File

@@ -120,6 +120,11 @@ select = [
"F", # flake8
]
# djlint lints/formats our jinja templates
# https://djlint.com/docs/configuration/
[tool.djlint]
indent = 2
# tbump is used to simplify and standardize the release process when updating
# the version, making a git commit and tag, and pushing changes.
#

View File

@@ -1,5 +1,2 @@
{% extends "error.html" %}
{% block error_detail %}
<p>Jupyter has lots of moons, but this is not one...</p>
{% endblock %}
{% block error_detail %}<p>Jupyter has lots of moons, but this is not one...</p>{% endblock %}

View File

@@ -1,55 +1,51 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block login_widget %}{% endblock %}
{% block main %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center">Accept sharing invitation</h1>
<p class="lead">
You ({{ user.name }}) have been invited to access {{ owner.name }}'s server
{%- if spawner.name %} ({{ spawner.name }}){%- endif %} at <a href="{{ spawner_url | safe }}">{{ spawner_url }}</a>
</p>
{% if not spawner_ready %}
<p class="alert alert-danger">
The server at {{ spawner_url }} is not currently running.
After accepting permission, you may need to ask {{ owner.name }}
to start the server before you can access it.
</p>
{% endif %}
<form method="POST" action="">
<div class="card">
<div class="card-header">
By accepting the invitation, you will be granted the following permissions,
restricted to this particular server:
</div>
<div class="card-body">
{# these are the 'real' inputs to the form -#}
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
<input type="hidden" name="code" value="{{ code }}" />
{% for scope_info in scope_descriptions -%}
<div class="form-check input-group">
<label>
<span>
{{ scope_info['description'] }}
{% if scope_info['filter'] %}
Applies to {{ scope_info['filter'] }}. {% endif %}
</span>
</label>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center">Accept sharing invitation</h1>
<p class="lead">
You ({{ user.name }}) have been invited to access {{ owner.name }}'s server
{%- if spawner.name %}({{ spawner.name }}){%- endif %} at <a href="{{ spawner_url | safe }}">{{ spawner_url }}</a>
</p>
{% if not spawner_ready %}
<p class="alert alert-danger">
The server at {{ spawner_url }} is not currently running.
After accepting permission, you may need to ask {{ owner.name }}
to start the server before you can access it.
</p>
{% endif %}
<form method="POST" action="">
<div class="card">
<div class="card-header">
By accepting the invitation, you will be granted the following permissions,
restricted to this particular server:
</div>
<div class="card-body">
{# these are the 'real' inputs to the form -#}
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
<input type="hidden" name="code" value="{{ code }}" />
{% for scope_info in scope_descriptions -%}
<div class="form-check input-group">
<label>
<span>
{{ scope_info['description'] }}
{% if scope_info['filter'] %}Applies to {{ scope_info['filter'] }}.{% endif %}
</span>
</label>
</div>
{% endfor -%}
</div>
<div class="card-footer">
<button type="submit" class="form-control btn btn-jupyter">Accept invitation</button>
<p class="small">
After accepting the invitation, you will be redirected to <a href="{{ next_url | safe }}">{{ next_url }}</a>.
</p>
</div>
</div>
</form>
</div>
{% endfor -%}
</div>
<div class="card-footer">
<button type="submit" class="form-control btn btn-jupyter" >Accept invitation</button>
<p class="small">
After accepting the invitation, you will be redirected to <a href="{{ next_url | safe }}">{{ next_url }}</a>.
</p>
</div>
</div>
</form>
</div></div></div>
{% endblock %}

View File

@@ -1,17 +1,13 @@
{% extends "page.html" %}
{% block main %}
<div id="react-admin-hook">
<script id="jupyterhub-admin-config">
<div id="react-admin-hook">
<script id="jupyterhub-admin-config">
window.api_page_limit = parseInt("{{ api_page_limit|safe }}")
window.base_url = "{{ base_url|safe }}"
</script>
<script src={{ static_url("js/admin-react.js") }}></script>
</div>
</script>
<script src={{ static_url("js/admin-react.js") }}></script>
</div>
{% endblock %}
{% block footer %}
<div class="py-2 px-4 bg-body-tertiary small version_footer">
JupyterHub {{ server_version }}
</div>
<div class="py-2 px-4 bg-body-tertiary small version_footer">JupyterHub {{ server_version }}</div>
{% endblock %}

View File

@@ -1,40 +1,19 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block login_widget %}{% endblock %}
{% block main %}
<div class="error">
{% block h1_error %}
<h1>
{{status_code}} : {{status_message}}
</h1>
{% endblock h1_error %}
{% block error_detail %}
{% if message %}
<p>
{{message}}
</p>
{% endif %}
{% if message_html %}
<p>
{{message_html | safe}}
</p>
{% endif %}
{% if extra_error_html %}
<p>
{{extra_error_html | safe}}
</p>
{% endif %}
{% endblock error_detail %}
</div>
<div class="error">
{% block h1_error %}
<h1>{{ status_code }} : {{ status_message }}</h1>
{% endblock h1_error %}
{% block error_detail %}
{% if message %}<p>{{ message }}</p>{% endif %}
{% if message_html %}<p>{{ message_html | safe }}</p>{% endif %}
{% if extra_error_html %}<p>{{ extra_error_html | safe }}</p>{% endif %}
{% endblock error_detail %}
</div>
{% endblock %}
{% block script %}
{{super()}}
{{ super() }}
<script type="text/javascript">
function _remove_redirects_from_url() {
if (window.location.search.length <= 1) {

View File

@@ -2,93 +2,89 @@
{% if announcement_home is string %}
{% set announcement = announcement_home %}
{% endif %}
{% block main %}
<div class="container">
<h1 class="sr-only">JupyterHub home page</h1>
<div class="row">
<div class="text-center">
{% if default_server.active %}
<a id="stop" role="button" class="btn btn-lg btn-danger">
Stop My Server
<div class="container">
<h1 class="sr-only">JupyterHub home page</h1>
<div class="row">
<div class="text-center">
{% if default_server.active %}<a id="stop" role="button" class="btn btn-lg btn-danger">Stop My Server</a>{% endif %}
<a id="start"
role="button"
class="btn btn-lg btn-primary"
href="{{ url }}">
{% if not default_server.active %}Start{% endif %}
My Server
</a>
{% endif %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ url }}">
{% if not default_server.active %}Start{% endif %}
My Server
</a>
</div>
</div>
{% if allow_named_servers %}
<h2>Named Servers</h2>
<p>
In addition to your default server,
you may have additional
{% if named_server_limit_per_user > 0 %}{{ named_server_limit_per_user }}{% endif %}
server(s) with names.
This allows you to have more than one server running at the same time.
</p>
{% set named_spawners = user.all_spawners(include_default=False)|list %}
<table class="server-table table table-striped">
<thead>
<tr>
<th>Server name</th>
<th>URL</th>
<th>Last activity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr class="home-server-row add-server-row">
<td colspan="4">
<input class="new-server-name"
aria-label="server name"
placeholder="name-your-server">
<button role="button"
type="button"
class="new-server-btn btn btn-xs btn-primary">Add New Server</button>
</td>
</tr>
{% for spawner in named_spawners %}
<tr class="home-server-row" data-server-name="{{ spawner.name }}">
{# name #}
<td>{{ spawner.name }}</td>
{# url #}
<td>
<a class="server-link {% if not spawner.ready %}hidden{% endif %}"
href="{{ user.server_url(spawner.name) }}">{{ user.server_url(spawner.name) }}</a>
</td>
{# activity #}
<td class='time-col'>
{% if spawner.last_activity %}
{{ spawner.last_activity.isoformat() + 'Z' }}
{% else %}
Never
{% endif %}
</td>
{# actions #}
<td>
<a role="button"
class="stop-server btn btn-xs btn-danger{% if not spawner.active %} hidden{% endif %}"
id="stop-{{ spawner.name }}">stop</a>
<a role="button"
class="start-server btn btn-xs btn-primary {% if spawner.active %}hidden{% endif %}"
id="start-{{ spawner.name }}"
href="{{ base_url }}spawn/{{ user.name }}/{{ spawner.name }}">start</a>
<button role="button"
class="delete-server btn btn-xs btn-danger{% if spawner.active %} hidden{% endif %}"
id="delete-{{ spawner.name }}">delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% if allow_named_servers %}
<h2>
Named Servers
</h2>
<p>
In addition to your default server,
you may have additional {% if named_server_limit_per_user > 0 %}{{ named_server_limit_per_user }} {% endif %}server(s) with names.
This allows you to have more than one server running at the same time.
</p>
{% set named_spawners = user.all_spawners(include_default=False)|list %}
<table class="server-table table table-striped">
<thead>
<tr>
<th>Server name</th>
<th>URL</th>
<th>Last activity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr class="home-server-row add-server-row">
<td colspan="4">
<input class="new-server-name" aria-label="server name" placeholder="name-your-server">
<button role="button" type="button" class="new-server-btn btn btn-xs btn-primary">
Add New Server
</button>
</td>
</tr>
{% for spawner in named_spawners %}
<tr class="home-server-row" data-server-name="{{ spawner.name }}">
{# name #}
<td>{{ spawner.name }}</td>
{# url #}
<td>
<a class="server-link {% if not spawner.ready %}hidden{% endif %}" href="{{ user.server_url(spawner.name) }}">
{{ user.server_url(spawner.name) }}
</a>
</td>
{# activity #}
<td class='time-col'>
{% if spawner.last_activity %}
{{ spawner.last_activity.isoformat() + 'Z' }}
{% else %}
Never
{% endif %}
</td>
{# actions #}
<td>
<a role="button" class="stop-server btn btn-xs btn-danger{% if not spawner.active %} hidden{% endif %}" id="stop-{{ spawner.name }}">stop</a>
<a role="button" class="start-server btn btn-xs btn-primary {% if spawner.active %} hidden{% endif %}" id="start-{{ spawner.name }}"
href="{{ base_url }}spawn/{{ user.name }}/{{ spawner.name }}"
>
start
</a>
<button role="button" class="delete-server btn btn-xs btn-danger{% if spawner.active %} hidden{% endif %}" id="delete-{{ spawner.name }}">delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
{% endblock main %}
{% block script %}
{{ super() }}
<script type="text/javascript">
require(["home"]);
</script>
{{ super() }}
<script type="text/javascript">require(["home"]);</script>
{% endblock %}

View File

@@ -2,112 +2,94 @@
{% if announcement_login is string %}
{% set announcement = announcement_login %}
{% endif %}
{% block login_widget %}
{% endblock %}
{% block login_widget %}{% endblock %}
{% block main %}
{% block login %}
<div id="login-main" class="container">
{% block login_container %}
{% if custom_html %}
{{ custom_html | safe }}
{% elif login_service %}
<div class="service-login">
<p id='insecure-login-warning' class='hidden'>
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
We strongly recommend enabling HTTPS for JupyterHub.
</p>
<a role="button" class='btn btn-jupyter btn-lg' href='{{ authenticator_login_url | safe }}'>
Sign in with {{login_service}}
</a>
</div>
{% else %}
<form action="{{ authenticator_login_url | safe }}" method="post" role="form">
<div class="auth-form-header">
<h1>Sign in</h1>
</div>
<div class='auth-form-body m-auto'>
<p id='insecure-login-warning' class='hidden'>
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
We strongly recommend enabling HTTPS for JupyterHub.
</p>
{% if login_error %}
<p class="login_error">
{{login_error}}
</p>
{% endif %}
<input type="hidden" name="_xsrf" value="{{ xsrf }}"/>
<label for="username_input">Username:</label>
<input
id="username_input"
type="text"
autocapitalize="off"
autocorrect="off"
autocomplete="username"
class="form-control"
name="username"
val="{{username}}"
autofocus="autofocus"
/>
<label for='password_input'>Password:</label>
<input
type="password"
class="form-control"
autocomplete="current-password"
name="password"
id="password_input"
/>
{% if authenticator.request_otp %}
<label for='otp_input'>{{ authenticator.otp_prompt }}</label>
<input
class="form-control"
autocomplete="one-time-password"
name="otp"
id="otp_input"
/>
{% endif %}
<div class="feedback-container">
<input
id="login_submit"
type="submit"
class='btn btn-jupyter form-control'
value='Sign in'
tabindex="3"
/>
<div class="feedback-widget hidden">
<i class="fa fa-spinner"></i>
</div>
{% block login %}
<div id="login-main" class="container">
{% block login_container %}
{% if custom_html %}
{{ custom_html | safe }}
{% elif login_service %}
<div class="service-login">
<p id='insecure-login-warning' class='hidden'>
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
We strongly recommend enabling HTTPS for JupyterHub.
</p>
<a role="button"
class='btn btn-jupyter btn-lg'
href='{{ authenticator_login_url | safe }}'>Sign in with {{ login_service }}</a>
</div>
{% else %}
<form action="{{ authenticator_login_url | safe }}"
method="post"
role="form">
<div class="auth-form-header">
<h1>Sign in</h1>
</div>
<div class='auth-form-body m-auto'>
<p id='insecure-login-warning' class='hidden'>
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
We strongly recommend enabling HTTPS for JupyterHub.
</p>
{% if login_error %}<p class="login_error">{{ login_error }}</p>{% endif %}
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
<label for="username_input">Username:</label>
<input id="username_input"
type="text"
autocapitalize="off"
autocorrect="off"
autocomplete="username"
class="form-control"
name="username"
val="{{ username }}"
autofocus="autofocus" />
<label for='password_input'>Password:</label>
<input type="password"
class="form-control"
autocomplete="current-password"
name="password"
id="password_input" />
{% if authenticator.request_otp %}
<label for='otp_input'>{{ authenticator.otp_prompt }}</label>
<input class="form-control"
autocomplete="one-time-password"
name="otp"
id="otp_input" />
{% endif %}
<div class="feedback-container">
<input id="login_submit"
type="submit"
class='btn btn-jupyter form-control'
value='Sign in'
tabindex="3" />
<div class="feedback-widget hidden">
<i class="fa fa-spinner"></i>
</div>
</div>
{% block login_terms %}
{% if login_term_url %}
<div id="login_terms" class="login_terms">
<input type="checkbox"
id="login_terms_checkbox"
name="login_terms_checkbox"
required />
{% block login_terms_text %}
{# allow overriding the text #}
By logging into the platform you accept the <a href="{{ login_term_url }}">terms and conditions</a>.
{% endblock login_terms_text %}
</div>
{% endif %}
{% endblock login_terms %}
</div>
</form>
{% endif %}
{% endblock login_container %}
</div>
{% block login_terms %}
{% if login_term_url %}
<div id="login_terms" class="login_terms">
<input type="checkbox" id="login_terms_checkbox" name="login_terms_checkbox" required />
{% block login_terms_text %} {# allow overriding the text #}
By logging into the platform you accept the <a href="{{ login_term_url }}">terms and conditions</a>.
{% endblock login_terms_text %}
</div>
{% endif %}
{% endblock login_terms %}
</div>
</form>
{% endif %}
{% endblock login_container %}
</div>
{% endblock login %}
{% endblock login %}
{% endblock %}
{% block script %}
{{ super() }}
<script>
{{ super() }}
<script>
if (window.location.protocol === "http:") {
// unhide http warning
var warning = document.getElementById('insecure-login-warning');
@@ -120,5 +102,5 @@ $('form').submit((e) => {
form.find('.feedback-container>*').toggleClass('hidden');
form.find('.feedback-widget>*').toggleClass('fa-pulse');
});
</script>
</script>
{% endblock %}

View File

@@ -2,13 +2,8 @@
{% if announcement_logout is string %}
{% set announcement = announcement_logout %}
{% endif %}
{% block main %}
<div id="logout-main" class="container">
<p>
Successfully logged out.
</p>
</div>
<div id="logout-main" class="container">
<p>Successfully logged out.</p>
</div>
{% endblock %}

View File

@@ -1,60 +1,62 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
{% block heading %}
<h1>
{% if failed %}
Spawn failed
{% else %}
Server not running
{% endif %}
</h1>
{% endblock %}
{% block message %}
<p>
{% if failed %}
The latest attempt to start your server {{ server_name }} has failed.
{% if failed_html_message %}
</p><p>{{ failed_html_message | safe }}</p><p>
{% elif failed_message %}
</p><p>{{ failed_message }}</p><p>
{% endif %}
Would you like to retry starting it?
{% else %}
Your server {{ server_name }} is not running.
{% if implicit_spawn_seconds %}
It will be restarted automatically.
If you are not redirected in a few seconds,
click below to launch your server.
{% else %}
Would you like to start it?
{% endif %}
{% endif %}
</p>
{% endblock %}
{% block start_button %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ spawn_url }}">
{% if failed %}
Relaunch
{% else %}
Launch
{% endif %}
Server {{ server_name }}
</a>
{% endblock %}
<div class="container">
<div class="row">
<div class="text-center">
{% block heading %}
<h1>
{% if failed %}
Spawn failed
{% else %}
Server not running
{% endif %}
</h1>
{% endblock %}
{% block message %}
<p>
{% if failed %}
The latest attempt to start your server {{ server_name }} has failed.
{% if failed_html_message %}
</p>
<p>{{ failed_html_message | safe }}</p>
<p>{% elif failed_message %}</p>
<p>{{ failed_message }}</p>
<p>
{% endif %}
Would you like to retry starting it?
{% else %}
Your server {{ server_name }} is not running.
{% if implicit_spawn_seconds %}
It will be restarted automatically.
If you are not redirected in a few seconds,
click below to launch your server.
{% else %}
Would you like to start it?
{% endif %}
{% endif %}
</p>
{% endblock %}
{% block start_button %}
<a id="start"
role="button"
class="btn btn-lg btn-primary"
href="{{ spawn_url }}">
{% if failed %}
Relaunch
{% else %}
Launch
{% endif %}
Server {{ server_name }}
</a>
{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
{{ super () }}
{% if implicit_spawn_seconds %}
<script type="text/javascript">
{{ super () }}
{% if implicit_spawn_seconds %}
<script type="text/javascript">
var spawn_url = "{{ spawn_url }}";
var implicit_spawn_seconds = {{ implicit_spawn_seconds }};
setTimeout(function () {
@@ -63,9 +65,7 @@
},
1000 * implicit_spawn_seconds
);
</script>
{% endif %}
<script type="text/javascript">
require(["not_running"]);
</script>
</script>
{% endif %}
<script type="text/javascript">require(["not_running"]);</script>
{% endblock script %}

View File

@@ -1,60 +1,47 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block login_widget %}{% endblock %}
{% block main %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="text-center">Authorize access</h1>
<p class="lead">
An application is requesting authorization to access data associated with your JupyterHub account
</p>
<p>
{{ oauth_client.description }} (oauth URL: {{ oauth_client.redirect_uri }})
would like permission to identify you.
{% if scope_descriptions | length == 1 and not scope_descriptions[0].scope %}
It will not be able to take actions on
your behalf.
{% endif %}
</p>
<form method="POST" action="">
<div class="card">
<div class="card-header">
<p class="h5">This will grant the application permission to:
</p>
</div>
<div class="card-body">
<input type="hidden" name="_xsrf" value="{{ xsrf }}"/>
{# these are the 'real' inputs to the form -#}
{% for scope in allowed_scopes %}
<input type="hidden" name="scopes" value="{{ scope }}"/>
{% endfor %}
{% for scope_info in scope_descriptions %}
<div class="checkbox input-group">
<label>
<input type="checkbox" name="raw-scopes" checked="true" title="This authorization is required"
disabled="disabled"
{# disabled because it's required #} />
<span>
{{ scope_info['description'] }}
{% if scope_info['filter'] %}
Applies to {{ scope_info['filter'] }}.
{% endif %}
</span>
</label>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="text-center">Authorize access</h1>
<p class="lead">An application is requesting authorization to access data associated with your JupyterHub account</p>
<p>
{{ oauth_client.description }} (oauth URL: {{ oauth_client.redirect_uri }})
would like permission to identify you.
{% if scope_descriptions | length == 1 and not scope_descriptions[0].scope %}
It will not be able to take actions on
your behalf.
{% endif %}
</p>
<form method="POST" action="">
<div class="card">
<div class="card-header">
<p class="h5">This will grant the application permission to:</p>
</div>
{% endfor %}
</div>
<div class="card-footer">
<button type="submit" class="form-control btn btn-jupyter mt-2">Authorize</button>
<div class="card-body">
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
{# these are the 'real' inputs to the form -#}
{% for scope in allowed_scopes %}<input type="hidden" name="scopes" value="{{ scope }}" />{% endfor %}
{% for scope_info in scope_descriptions %}
<div class="checkbox input-group">
<label>
<input type="checkbox" name="raw-scopes" checked="true" title="This authorization is required"
disabled="disabled"
{# disabled because it's required #} />
<span>
{{ scope_info['description'] }}
{% if scope_info['filter'] %}Applies to {{ scope_info['filter'] }}.{% endif %}
</span>
</label>
</div>
{% endfor %}
</div>
<div class="card-footer">
<button type="submit" class="form-control btn btn-jupyter mt-2">Authorize</button>
</div>
</div>
</form>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,51 +1,55 @@
{% macro modal(title, btn_label=None, btn_class="btn-primary") %}
{% set key = title.replace(' ', '-').lower() %}
{% set btn_label = btn_label or title %}
<div class="modal fade" id="{{key}}-dialog" tabindex="-1" role="dialog" aria-labelledby="{{key}}-label" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="{{key}}-label">{{title}}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ caller() }}
</div>
<div class="modal-footer">
<button type="button" class="btn {{btn_class}}" data-bs-dismiss="modal" data-dismiss="modal">{{btn_label}}</button>
{% set key = title.replace(' ', '-').lower() %}
{% set btn_label = btn_label or title %}
<div class="modal fade"
id="{{ key }}-dialog"
tabindex="-1"
role="dialog"
aria-labelledby="{{ key }}-label"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="{{ key }}-label">
{{ title }}
</h1>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">{{ caller() }}</div>
<div class="modal-footer">
<button type="button"
class="btn {{ btn_class }}"
data-bs-dismiss="modal"
data-dismiss="modal">{{ btn_label }}</button>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}
<!DOCTYPE HTML>
<html lang="en">
<head>
<head>
<meta charset="utf-8">
<title>{% block title %}JupyterHub{% endblock %}</title>
<title>
{% block title %}JupyterHub{% endblock %}
</title>
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block stylesheet %}
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
{% endblock %}
{% block favicon %}
<link rel="icon" href="{{ static_url("favicon.ico") }}" type="image/x-icon">
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css" />
{% endblock %}
{% block favicon %}<link rel="icon" href="{{ static_url("favicon.ico") }}" type="image/x-icon">{% endblock %}
{% block scripts %}
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.bundle.min.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.bundle.min.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
{% endblock %}
<script>
require.config({
{% if version_hash %}
urlArgs: "v={{version_hash}}",
{% endif %}
{% if version_hash %}urlArgs: "v={{version_hash}}",{% endif %}
baseUrl: '{{static_url("js", include_version=False)}}',
paths: {
components: '../components',
@@ -54,7 +58,6 @@
},
});
</script>
<script type="text/javascript">
window.jhdata = {
base_url: "{{base_url}}",
@@ -79,109 +82,96 @@
{% endif %}
xsrf_token: "{{ xsrf_token }}",
}
</script>
{% block meta %}
{% endblock %}
</head>
<body>
<noscript>
<div id='noscript'>
JupyterHub requires JavaScript.<br>
Please enable it to proceed.
</div>
</noscript>
{% block nav_bar %}
<nav class="navbar navbar-expand-sm bg-body-tertiary mb-4">
<div class="container-fluid">
{% block logo %}
<span id="jupyterhub-logo" class="navbar-brand">
<a href="{{logo_url or base_url}}"><img src='{{base_url}}logo' alt='JupyterHub logo' class='jpy-logo' title='Home'/></a>
</span>
{% endblock %}
{% if user %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#thenavbar" aria-controls="thenavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
{% endif %}
<div class="collapse navbar-collapse" id="thenavbar">
{% if user %}
<ul class="navbar-nav me-auto mb-0">
{% block nav_bar_left_items %}
<li class="nav-item"><a class="nav-link" href="{{base_url}}home">Home</a></li>
<li class="nav-item"><a class="nav-link" href="{{base_url}}token">Token</a></li>
{% if 'admin-ui' in parsed_scopes %}
<li class="nav-item"><a class="nav-link" href="{{base_url}}admin">Admin</a></li>
{% endif %}
{% if services %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">Services</a>
<ul class="dropdown-menu">
{% for service in services %}
{% block service scoped %}
<li><a class="dropdown-item" href="{{service.href}}">{{service.name}}</a></li>
{% endblock %}
{% endfor %}
</ul>
</li>
{% endif %}
{% endblock %}
</ul>
{% endif %}
<ul class="nav navbar-nav me-2">
{% block nav_bar_right_items %}
<li class="nav-item">
{% block login_widget %}
<span id="login_widget">
{% if user %}
<span class="navbar-text">{{user.name}}</span>
<a id="logout" role="button" class="btn btn-sm btn-outline-dark" href="{{logout_url}}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
{% else %}
<a id="login" role="button" class="btn btn-sm btn-outline-dark" href="{{login_url}}">Login</a>
{% endif %}
</span>
{% endblock %}
</li>
{% endblock %}
</ul>
</div>
{% block header %}
{% endblock %}
</div>
</nav>
</script>
{% block meta %}
{% endblock %}
{% block announcement %}
{% if announcement %}
<div class="container text-center announcement alert alert-warning">
{{ announcement | safe }}
</head>
<body>
<noscript>
<div id='noscript'>
JupyterHub requires JavaScript.
<br>
Please enable it to proceed.
</div>
</noscript>
{% block nav_bar %}
<nav class="navbar navbar-expand-sm bg-body-tertiary mb-4">
<div class="container-fluid">
{% block logo %}
<span id="jupyterhub-logo" class="navbar-brand">
<a href="{{ logo_url or base_url }}">
<img src='{{ base_url }}logo' alt='JupyterHub logo' class='jpy-logo' title='Home' />
</a>
</span>
{% endblock %}
{% if user %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#thenavbar" aria-controls="thenavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
{% endif %}
<div class="collapse navbar-collapse" id="thenavbar">
{% if user %}
<ul class="navbar-nav me-auto mb-0">
{% block nav_bar_left_items %}
<li class="nav-item">
<a class="nav-link" href="{{ base_url }}home">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ base_url }}token">Token</a>
</li>
{% if 'admin-ui' in parsed_scopes %}
<li class="nav-item">
<a class="nav-link" href="{{ base_url }}admin">Admin</a>
</li>
{% endif %}
{% if services %}
<li class="nav-item dropdown">
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">Services</a>
<ul class="dropdown-menu">
{% for service in services %}
{% block service scoped %}
<li>
<a class="dropdown-item" href="{{ service.href }}">{{ service.name }}</a>
</li>
{% endblock %}
{% endfor %}
</ul>
</li>
{% endif %}
{% endblock %}
{% block main %}
</ul>
{% endif %}
<ul class="nav navbar-nav me-2">
{% block nav_bar_right_items %}
<li class="nav-item">
{% block login_widget %}
<span id="login_widget">
{% if user %}
<span class="navbar-text">{{ user.name }}</span>
<a id="logout" role="button" class="btn btn-sm btn-outline-dark" href="{{ logout_url }}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
{% else %}
<a id="login" role="button" class="btn btn-sm btn-outline-dark" href="{{ login_url }}">Login</a>
{% endif %}
</span>
{% endblock %}
{% block footer %}
</li>
{% endblock %}
</ul>
</div>
{% block header %}{% endblock %}
</div>
</nav>
{% endblock %}
{% block announcement %}
{% if announcement %}
<div class="container text-center announcement alert alert-warning">{{ announcement | safe }}</div>
{% endif %}
{% endblock %}
{% block main %}{% endblock %}
{% block footer %}{% endblock %}
{% call modal('Error', btn_label='OK') %}
<div class="ajax-error alert-danger">
The error
</div>
<div class="ajax-error alert-danger">The error</div>
{% endcall %}
{% block script %}
{% endblock %}
{% block script %}{% endblock %}
</body>
</html>

View File

@@ -2,43 +2,39 @@
{% if announcement_spawn is string %}
{% set announcement = announcement_spawn %}
{% endif %}
{% block main %}
<div class="container">
{% block heading %}
<div class="row text-center">
<h1>Server Options</h1>
</div>
{% endblock %}
<div class="row justify-content-center">
<div class="col-md-8">
{% if for_user and user.name != for_user.name -%}
<p>Spawning server for {{ for_user.name }}</p>
{% endif -%}
{% if error_message -%}
<p class="spawn-error-msg alert alert-danger">
Error: {{error_message}}
</p>
{% endif %}
<form enctype="multipart/form-data" id="spawn_form" action="{{ url | safe }}" method="post" role="form">
{{spawner_options_form | safe}}
<br>
<div class="feedback-container">
<button type="submit" class="btn btn-jupyter form-control">Start</button>
<div class="feedback-widget hidden">
<i class="fa fa-spinner"></i>
</div>
<div class="container">
{% block heading %}
<div class="row text-center">
<h1>Server Options</h1>
</div>
</form>
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script>
{% endblock %}
<div class="row justify-content-center">
<div class="col-md-8">
{% if for_user and user.name != for_user.name -%}
<p>Spawning server for {{ for_user.name }}</p>
{% endif -%}
{% if error_message -%}<p class="spawn-error-msg alert alert-danger">Error: {{ error_message }}</p>{% endif %}
<form enctype="multipart/form-data"
id="spawn_form"
action="{{ url | safe }}"
method="post"
role="form">
{{ spawner_options_form | safe }}
<br>
<div class="feedback-container">
<button type="submit" class="btn btn-jupyter form-control">Start</button>
<div class="feedback-widget hidden">
<i class="fa fa-spinner"></i>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script>
// setup onSubmit feedback
$('form').submit((e) => {
var form = $(e.target);
@@ -46,5 +42,5 @@ $('form').submit((e) => {
form.find('.feedback-container>*').toggleClass('hidden');
form.find('.feedback-widget>*').toggleClass('fa-pulse');
});
</script>
{% endblock %}
</script>
{% endblock %}

View File

@@ -1,37 +1,39 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
{% block message %}
<p>Your server is starting up.</p>
<p>You will be redirected automatically when it's ready for you.</p>
{% endblock %}
<div class="progress">
<div id="progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="sr-only"><span id="sr-progress">0%</span> Complete</span>
<div class="container">
<div class="row">
<div class="text-center">
{% block message %}
<p>Your server is starting up.</p>
<p>You will be redirected automatically when it's ready for you.</p>
{% endblock %}
<div class="progress">
<div id="progress-bar"
class="progress-bar"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
style="width: 0%">
<span class="sr-only"><span id="sr-progress">0%</span> Complete</span>
</div>
</div>
<p id="progress-message"></p>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<details id="progress-details">
<summary>Event log</summary>
<div id="progress-log"></div>
</details>
</div>
<p id="progress-message"></p>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-8">
<details id="progress-details">
<summary>Event log</summary>
<div id="progress-log"></div>
</details>
</div>
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
{{ super() }}
<script type="text/javascript">
require(["jquery"], function ($) {
$("#refresh").click(function () {
window.location.reload();
@@ -90,5 +92,5 @@ require(["jquery"], function ($) {
// signal that page has finished loading (mostly for tests)
window._jupyterhub_page_loaded = true;
});
</script>
</script>
{% endblock %}

View File

@@ -1,25 +1,23 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
{% block message %}
<p>Your server is stopping.</p>
<p>You will be able to start it again once it has finished stopping.</p>
{% endblock message %}
<p><i class="fa fa-spinner fa-pulse fa-fw fa-3x" aria-hidden="true"></i></p>
<a role="button" id="refresh" class="btn btn-lg btn-primary" href="#">refresh</a>
<div class="container">
<div class="row">
<div class="text-center">
{% block message %}
<p>Your server is stopping.</p>
<p>You will be able to start it again once it has finished stopping.</p>
{% endblock message %}
<p>
<i class="fa fa-spinner fa-pulse fa-fw fa-3x" aria-hidden="true"></i>
</p>
<a role="button" id="refresh" class="btn btn-lg btn-primary" href="#">refresh</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block script %}
{{ super() }}
<script type="text/javascript">
{{ super() }}
<script type="text/javascript">
require(["jquery"], function ($) {
$("#refresh").click(function () {
window.location.reload();
@@ -28,5 +26,5 @@ require(["jquery"], function ($) {
window.location.reload();
}, 5000);
});
</script>
</script>
{% endblock %}

View File

@@ -1,58 +1,47 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<h1 class="sr-only">Manage JupyterHub Tokens</h1>
<div class="row justify-content-center">
<form id="request-token-form" class="col-lg-6">
<div class="form-group">
<label for="token-note" class="form-label">Note</label>
<input
id="token-note"
class="form-control"
placeholder="note to identify your new token">
<small id="note-note" class="form-text">
This note will help you keep track of what your tokens are for.
</small>
<br/>
<label for="token-expiration-seconds" class="form-label">Token expires in</label>
{% block expiration_options %}
<select id="token-expiration-seconds"
class="form-select">
<!-- unit used for each value is `seconds` -->
<option value="3600">1 Hour</option>
<option value="86400">1 Day</option>
<option value="604800">1 Week</option>
<option value="" selected="selected">Never</option>
</select>
{% endblock expiration_options %}
<small id="note-expires-at" class="form-text">
You can configure when your token will expire.
</small>
<br/>
<label for="token-scopes" class="form-label">Permissions</label>
<input id="token-scopes" class="form-control" placeholder="list of scopes for the token to have, separated by space">
<small id="note-token-scopes" class="form-text">
You can limit the permissions of the token so it can only do what you want it to.
If none are specified, the token will have permission to do everything you can do.
See the <a href="https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes">JupyterHub documentation for a list of available scopes</a>.
</small>
</div>
<div class="text-center m-4">
<button type="submit" class="btn btn-lg btn-jupyter">
Request new API token
</button>
</div>
</form>
</div>
<div class="row justify-content-center">
<div class="container">
<h1 class="sr-only">Manage JupyterHub Tokens</h1>
<div class="row justify-content-center">
<form id="request-token-form" class="col-lg-6">
<div class="form-group">
<label for="token-note" class="form-label">Note</label>
<input id="token-note"
class="form-control"
placeholder="note to identify your new token">
<small id="note-note" class="form-text">This note will help you keep track of what your tokens are for.</small>
<br />
<label for="token-expiration-seconds" class="form-label">Token expires in</label>
{% block expiration_options %}
<select id="token-expiration-seconds" class="form-select">
<!-- unit used for each value is `seconds` -->
<option value="3600">1 Hour</option>
<option value="86400">1 Day</option>
<option value="604800">1 Week</option>
<option value="" selected="selected">Never</option>
</select>
{% endblock expiration_options %}
<small id="note-expires-at" class="form-text">You can configure when your token will expire.</small>
<br />
<label for="token-scopes" class="form-label">Permissions</label>
<input id="token-scopes"
class="form-control"
placeholder="list of scopes for the token to have, separated by space">
<small id="note-token-scopes" class="form-text">
You can limit the permissions of the token so it can only do what you want it to.
If none are specified, the token will have permission to do everything you can do.
See the <a href="https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes">JupyterHub documentation for a list of available scopes</a>.
</small>
</div>
<div class="text-center m-4">
<button type="submit" class="btn btn-lg btn-jupyter">Request new API token</button>
</div>
</form>
</div>
<div class="row justify-content-center">
<div id="token-area" class="col-lg-6" style="display: none;">
<div class="card">
<div class="card-header">
Your new API Token
</div>
<div class="card-header">Your new API Token</div>
<div class="card-body">
<p class="card-title text-center">
<span id="token-result"></span>
@@ -64,138 +53,129 @@
</div>
</div>
</div>
</div>
{% if api_tokens %}
<div class="row" id="api-tokens-section">
<div class="col">
<h2>API Tokens</h2>
<p>
These are tokens with access to the JupyterHub API.
Permissions for each token may be viewed via the JupyterHub tokens API.
Revoking the API token for a running server will require restarting that server.
</p>
<table class="table table-striped" id="api-tokens-table">
<thead>
<tr>
<th>Note</th>
<th>Permissions</th>
<th>Last used</th>
<th>Created</th>
<th>Expires</th>
</tr>
</thead>
<tbody>
{% for token in api_tokens %}
<tr class="token-row container" data-token-id="{{token.api_id}}">
{% block token_row scoped %}
<td class="note-col col">{{token.note}}</td>
<td class="scope-col col">
<details>
<summary>scopes</summary>
{% for scope in token.scopes %}
<pre class="token-scope">{{ scope }}</pre>
{% endfor %}
</details>
</td>
<td class="time-col col">
{%- if token.last_activity -%}
{{ token.last_activity.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="time-col col">
{%- if token.created -%}
{{ token.created.isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="time-col col">
{%- if token.expires_at -%}
{{ token.expires_at.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="col text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
</td>
{% endblock token_row %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if oauth_clients %}
<div class="row" id="oauth-clients-section">
<h2>Authorized Applications</h2>
<p>
These are applications that use OAuth with JupyterHub
to identify users (mostly notebook servers).
OAuth tokens can generally only be used to identify you,
not take actions on your behalf.
</p>
<table class="table table-striped" id="oauth-tokens-table">
<thead>
<tr>
<th>Application</th>
<th>Permissions</th>
<th>Last used</th>
<th>First authorized</th>
</tr>
</thead>
<tbody>
{% for client in oauth_clients %}
<tr class="token-row"
data-token-id="{{ client['token_id'] }}">
{% block client_row scoped %}
<td class="note-col col-sm-4">{{ client['description'] }}</td>
<td class="scope-col col-sm-1">
<details>
<summary>scopes</summary>
{# create set of scopes on all tokens -#}
{# sum concatenates all token.scopes into a single list -#}
{# then filter to unique set and sort -#}
{% for scope in client.tokens | sum(attribute="scopes", start=[]) | unique | sort %}
<pre class="token-scope">{{ scope }}</pre>
{% endfor %}
</details>
</td>
<td class="time-col col-sm-3">
{%- if client['last_activity'] -%}
{{ client['last_activity'].isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if client['created'] -%}
{{ client['created'].isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
{% endblock client_row %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock main %}
{% block script %}
{{ super() }}
<script type="text/javascript">
require(["token"]);
</script>
{% endblock script %}
</div>
{% if api_tokens %}
<div class="row" id="api-tokens-section">
<div class="col">
<h2>API Tokens</h2>
<p>
These are tokens with access to the JupyterHub API.
Permissions for each token may be viewed via the JupyterHub tokens API.
Revoking the API token for a running server will require restarting that server.
</p>
<table class="table table-striped" id="api-tokens-table">
<thead>
<tr>
<th>Note</th>
<th>Permissions</th>
<th>Last used</th>
<th>Created</th>
<th>Expires</th>
</tr>
</thead>
<tbody>
{% for token in api_tokens %}
<tr class="token-row container" data-token-id="{{ token.api_id }}">
{% block token_row scoped %}
<td class="note-col col">{{ token.note }}</td>
<td class="scope-col col">
<details>
<summary>scopes</summary>
{% for scope in token.scopes %}<pre class="token-scope">{{ scope }}</pre>{% endfor %}
</details>
</td>
<td class="time-col col">
{%- if token.last_activity -%}
{{ token.last_activity.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="time-col col">
{%- if token.created -%}
{{ token.created.isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="time-col col">
{%- if token.expires_at -%}
{{ token.expires_at.isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="col text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
</td>
{% endblock token_row %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% if oauth_clients %}
<div class="row" id="oauth-clients-section">
<h2>Authorized Applications</h2>
<p>
These are applications that use OAuth with JupyterHub
to identify users (mostly notebook servers).
OAuth tokens can generally only be used to identify you,
not take actions on your behalf.
</p>
<table class="table table-striped" id="oauth-tokens-table">
<thead>
<tr>
<th>Application</th>
<th>Permissions</th>
<th>Last used</th>
<th>First authorized</th>
</tr>
</thead>
<tbody>
{% for client in oauth_clients %}
<tr class="token-row" data-token-id="{{ client['token_id'] }}">
{% block client_row scoped %}
<td class="note-col col-sm-4">{{ client['description'] }}</td>
<td class="scope-col col-sm-1">
<details>
<summary>scopes</summary>
{# create set of scopes on all tokens -#}
{# sum concatenates all token.scopes into a single list -#}
{# then filter to unique set and sort -#}
{% for scope in client.tokens | sum(attribute="scopes", start=[]) | unique | sort %}
<pre class="token-scope">{{ scope }}</pre>
{% endfor %}
</details>
</td>
<td class="time-col col-sm-3">
{%- if client['last_activity'] -%}
{{ client['last_activity'].isoformat() + 'Z' }}
{%- else -%}
Never
{%- endif -%}
</td>
<td class="time-col col-sm-3">
{%- if client['created'] -%}
{{ client['created'].isoformat() + 'Z' }}
{%- else -%}
N/A
{%- endif -%}
</td>
<td class="col-sm-1 text-center">
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
{% endblock client_row %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock main %}
{% block script %}
{{ super() }}
<script type="text/javascript">require(["token"]);</script>
{% endblock script %}