-
Notifications
You must be signed in to change notification settings - Fork 4
Use better patterns for login view #671
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,57 +19,56 @@ | |
| {% block content_body %} | ||
| {% get_providers as socialaccount_providers %} | ||
|
|
||
| <div class="ui basic horizontally fitted segment"> | ||
| <div class="ui form"> | ||
| <div class="field"> | ||
| <label> | ||
| {% block authentication_header_text %} | ||
| {% trans "Log in using:" %} | ||
| {% endblock authentication_header_text %} | ||
| </label> | ||
| <div data-bind="using: LoginView({last_tab: '{{ last_login_tab }}', last_method: '{{ last_login_method }}'})"> | ||
| <div class="ui basic horizontally fitted segment"> | ||
| <div class="ui form"> | ||
| <div class="field"> | ||
| <label> | ||
| {% block authentication_header_text %} | ||
| {% trans "Log in using:" %} | ||
| {% endblock authentication_header_text %} | ||
| </label> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="ui small stackable teal centered fluid wrapping menu" data-bind="semanticui: {tabs: { | ||
| {% if last_login_tab %}autoTabActivation: '{{ last_login_tab }}'{% endif %} | ||
| }}"> | ||
|
|
||
| <a class="item" data-tab="vcs"> | ||
| <i class="fas fa-cloud icon"></i> | ||
| {% block authentication_vcs_text %} | ||
| {% trans "Connected account" %} | ||
| {% endblock authentication_vcs_text %} | ||
| </a> | ||
|
|
||
| {# If allowed providers is given, disable the email menu item #} | ||
| <a class="{% if allowed_providers %}disabled{% endif %} item" | ||
| {% if last_login_tab == "email" %} data-tooltip="Last used" data-variation="teal visible" data-position="top center" {% endif %} | ||
| data-tab="email"> | ||
| <i class="fas fa-envelope icon"></i> | ||
| {% block authentication_email_text %} | ||
| {% trans "Email" %} | ||
| {% endblock authentication_email_text %} | ||
| </a> | ||
|
|
||
| {% if USE_ORGANIZATIONS %} | ||
| <a class="item" data-tab="sso"> | ||
| <i class="fas fa-shield-alt icon"></i> | ||
| {% trans "Single sign-on" %} | ||
| <div class="ui small stackable teal centered fluid wrapping menu" | ||
| data-bind="semanticui: {tabs: { autoTabActivation: last_tab() }}"> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not, Knockout has observable declaration order mostly handled for us, and one of the reasons we want to stick to using single patterns. The data bind layer will only be activated within KO, as the view executes, but the view will also wait for all of the view observables to resolve before allowing declaring the value to this binding. It is possible to introduce race conditions when setting an observable value async, here you can notice the binding resolving more than once. For example an observable set from an API response, view instantiation will fire one variable change event for an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to clarify, the order of this is: everything in |
||
|
|
||
| <a class="item" data-tab="vcs"> | ||
| <i class="fas fa-cloud icon"></i> | ||
| {% block authentication_vcs_text %} | ||
| {% trans "Connected account" %} | ||
| {% endblock authentication_vcs_text %} | ||
| </a> | ||
| {% endif %} | ||
|
|
||
| {% if project_password_url %} | ||
| <a class="item" href="{{ project_password_url }}"> | ||
| <i class="fas fa-lock icon"></i> | ||
| {% block authentication_password_text %} | ||
| {% trans "Password" %} | ||
| {% endblock authentication_password_text %} | ||
|
|
||
| {# If allowed providers is given, disable the email menu item #} | ||
| <a class="{% if allowed_providers %}disabled{% endif %} item" | ||
| data-bind="semanticui: { popup: popup_login_method('email') }" | ||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| data-tab="email"> | ||
| <i class="fas fa-envelope icon"></i> | ||
| {% block authentication_email_text %} | ||
| {% trans "Email" %} | ||
| {% endblock authentication_email_text %} | ||
| </a> | ||
| {% endif %} | ||
|
|
||
| {% if USE_ORGANIZATIONS %} | ||
| <a class="item" data-tab="sso"> | ||
| <i class="fas fa-shield-alt icon"></i> | ||
| {% trans "Single sign-on" %} | ||
| </a> | ||
| {% endif %} | ||
|
|
||
| {% if project_password_url %} | ||
| <a class="item" href="{{ project_password_url }}"> | ||
| <i class="fas fa-lock icon"></i> | ||
| {% block authentication_password_text %} | ||
| {% trans "Password" %} | ||
| {% endblock authentication_password_text %} | ||
| </a> | ||
| {% endif %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div data-bind="using: LoginView()"> | ||
| <div class="ui basic center aligned tab segment" data-tab="vcs"> | ||
| <div class="ui relaxed list"> | ||
| {% block authentication_vcs %} | ||
|
|
@@ -103,8 +102,7 @@ | |
| <a class="item" href="{% url 'account_reset_password' %}">{% trans "Forgot your password?" %}</a> | ||
| <div class="right menu"> | ||
| <button class="ui primary button" | ||
| data-bind="click: save_login_method" | ||
| data-provider="email" | ||
| data-bind="click: save_login_method.bind($data, 'email')" | ||
| type="submit">{% trans "Log in" %}</button> | ||
| </div> | ||
| </div> | ||
|
|
@@ -118,11 +116,9 @@ | |
| <a href="{% url "saml_resolve_login" %}{% if redirect_field_value %}?{{ redirect_field_name }}={{ redirect_field_value }}{% endif %}"> | ||
|
|
||
| <button class="ui button" | ||
| data-bind="click: save_login_method" | ||
| data-provider="sso" | ||
| type="submit" | ||
| {% if last_login_method == "sso" %} data-tooltip="Last used" data-variation="teal visible" data-position="right center" {% endif %} | ||
| title="Single sign-on"> | ||
| data-bind="click: save_login_method.bind($data, 'sso'), semanticui: { popup: popup_login_method('sso') }" | ||
| title="{% trans "Single sign-on" %}"> | ||
| <i class="fas fa-shield-alt icon" aria-hidden="true"></i> | ||
| {% trans "Log in using single sign-on" %} | ||
| </button> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,35 +1,37 @@ | ||
| {% extends "profiles/base_edit.html" %} | ||
|
|
||
| {% load i18n %} | ||
| {% load trans blocktrans from i18n %} | ||
|
|
||
| {% block title %}{% trans "Connected services" %}{% endblock %} | ||
| {% block edit_content_header %}{% trans "Connected services" %}{% endblock %} | ||
| {% block profile_admin_social_accounts %}active{% endblock %} | ||
| {% block title %} | ||
| {% trans "Connected services" %} | ||
| {% endblock title %} | ||
| {% block edit_content_header %} | ||
| {% trans "Connected services" %} | ||
| {% endblock edit_content_header %} | ||
| {% block profile_admin_social_accounts %} | ||
| active | ||
| {% endblock profile_admin_social_accounts %} | ||
|
|
||
| {% block edit_content %} | ||
| {% include "socialaccount/partials/social_account_list.html" with objects=form.accounts %} | ||
|
|
||
| <div class="ui tiny modal" data-modal-id="socialaccount-connections"> | ||
| <i class="fa-solid fa-close close icon"></i> | ||
| <div class="header"> | ||
| {% trans "Add a connection" %} | ||
| </div> | ||
| <div class="header">{% trans "Add a connection" %}</div> | ||
| <div class="content center aligned"> | ||
| <div class="ui basic center aligned segment"> | ||
| <p> | ||
| {% blocktrans trimmed %} | ||
| You can connect your account with any of the following providers: | ||
| {% endblocktrans %} | ||
| </p> | ||
| <div class="ui relaxed list"> | ||
| <div class="ui relaxed list" data-bind="using: SocialAccountView()"> | ||
| {% include "socialaccount/snippets/provider_list.html" with process="connect" next="" %} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div class="actions"> | ||
| <div class="ui cancel button"> | ||
| {% trans "Cancel" %} | ||
| </div> | ||
| <div class="ui cancel button">{% trans "Cancel" %}</div> | ||
| </div> | ||
| </div> | ||
| {% endblock %} | ||
| {% endblock edit_content %} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,142 @@ | ||
| import ko from "knockout"; | ||
| import { msg } from "@lit/localize"; | ||
|
|
||
| import { Registry } from "../application/registry"; | ||
|
|
||
| /** | ||
| * LoginView manages login-related functionality including saving the | ||
| * last used login method to a cookie for session persistence. | ||
| * Listing view for social account connections. | ||
| * | ||
| * This view is subclassed by the :js:cls:`LoginView`, as the templates that | ||
| * use this code are shared between the social account connection listing view | ||
| * and the login view. On the social account listing we don't want the same | ||
| * features for last login method etc so this view overloads the functions | ||
| * used. | ||
| */ | ||
| export class SocialAccountView { | ||
| static view_name = "SocialAccountView"; | ||
|
|
||
| constructor(options) { | ||
| this.github_modal_config = ko.observable(); | ||
| } | ||
|
|
||
| show_github_modal() { | ||
| this.github_modal_config((modal) => modal("show")); | ||
| } | ||
|
|
||
| // No-op to skip popup setup | ||
| popup_login_method() {} | ||
|
|
||
| save_login_method() { | ||
| return true; | ||
| } | ||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * LoginView saves a cookie for the last login method. | ||
| * | ||
| * Usage in a binding context: | ||
| * Usage: | ||
| * | ||
| * .. code:: html | ||
| * | ||
| * <div data-bind="using: LoginView()"> | ||
| * <form method="post" action="..."> | ||
| * <button data-bind="click: save_login_method" data-provider="github"> | ||
| * <button data-bind="click: save_login_method.bind('email'), semanticui: { popup: popup_login_method('email') }"> | ||
humitos marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * Log in using GitHub | ||
| * </button> | ||
| * </form> | ||
| * </div> | ||
| */ | ||
| export class LoginView { | ||
| export class LoginView extends SocialAccountView { | ||
| static view_name = "LoginView"; | ||
|
|
||
| constructor() {} | ||
| constructor(options) { | ||
| super(options); | ||
|
|
||
| /** @observable {string} Last tab to be selected. Comes from view */ | ||
| this.last_tab = ko.observable(options?.last_tab || "vcs"); | ||
| /** @observable {string} Last method to be used. Comes from view */ | ||
| // XXX fix default, this is just for testing | ||
| this.last_method = ko.observable(options?.last_method || "githubapp"); | ||
|
|
||
| // This is an named lookup for observables, one for each method. It is | ||
| // populated by the template code as each popup is configured. | ||
| this.popups = {}; | ||
| } | ||
|
|
||
| /** | ||
| * Save the provider used for login. | ||
| * Add popup for login method | ||
| * | ||
| * This adds an observable using by the semanticui binding to add and | ||
| * manipulate a popup module on the element. | ||
| * | ||
| * Accepts multiple method ids just because we have a GitHub sub-modal right | ||
| * now. This can be removed eventually. | ||
| * | ||
| * @param {string|Array.<string>} method - Method id or list of method ids | ||
| * @param {string} position - Position to pass to SUI popup position attribute | ||
| * @param {string} method - Method id | ||
| */ | ||
| popup_login_method(method, position = "top center", fuzzy = true) { | ||
agjohnson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| this.popups[method] = ko.observable((popup) => { | ||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // This is using the KO data binding method for utilizing SUI module | ||
| // behaviors. Passing this an anonymous function provides access to | ||
| // `$(element).popup()` essentially. | ||
| // First set up the element as a manual popup, then manually show it. | ||
| popup({ | ||
| content: msg(`Last used`), | ||
| position: position, | ||
| variation: "mini teal", | ||
| closable: true, | ||
| preserve: true, | ||
| on: "manual", | ||
| }); | ||
| if (!Array.isArray(method) && this.last_method() == method) { | ||
| popup("show"); | ||
| } else if (Array.isArray(method) && method.includes(this.last_method())) { | ||
| popup("show"); | ||
| } | ||
| }); | ||
| return this.popups[method](); | ||
| } | ||
|
|
||
| /** | ||
| * Save a cookie to track last login method. | ||
| * | ||
| * This could be used like: | ||
| * | ||
| * .. code:: html | ||
| * | ||
| * <form method="post" action="..."> | ||
| * <button data-bind="click: save_login_method" data-provider="github"> | ||
| * <button data-bind="click: save_login_method.bind('email')"> | ||
| * Log in using GitHub | ||
| * </button> | ||
| * </form> | ||
| * | ||
| * @param {Object} data - Context data | ||
| * @param {Event} event - Click event | ||
| * @param {string} method - Method id | ||
| * @returns {knockout_click} | ||
| */ | ||
| save_login_method(data, event) { | ||
| const elem = event.currentTarget; | ||
| save_login_method(method) { | ||
| if (window.isSecureContext) { | ||
| console.debug("Setting last login method: ", elem.dataset.provider); | ||
| cookieStore.set("last-login-method", elem.dataset.provider); | ||
| console.debug("Setting last login method:", method); | ||
| cookieStore.set("last-login-method", method); | ||
| } else { | ||
| console.debug("Insecure, not setting last login method:", method); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| // On top of showing the GitHub modal, show any popups for GitHub providers as | ||
| // well. This avoids requiring the popups to always be visible and avoids | ||
| // manual removal of the popups. | ||
| show_github_modal() { | ||
| super.show_github_modal(); | ||
| const last_method = this.last_method(); | ||
| if (["github", "githubapp"].includes(last_method)) { | ||
| console.debug(last_method, this.popups, this.popups[last_method]); | ||
| this.popups[last_method]((popup) => popup("show")); | ||
humitos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| Registry.add_view(SocialAccountView); | ||
| Registry.add_view(LoginView); | ||
Uh oh!
There was an error while loading. Please reload this page.