From 2a3f0f3befc1df95e82e5203cc15100054dbf1cb Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 19 Dec 2024 14:49:36 -0500 Subject: [PATCH] UX: refactor IP lookup using DMenu to improve layout and positioning (#30374) --- .../admin/addon/components/ip-lookup.gjs | 266 ++++++++++++++++++ .../admin/addon/components/ip-lookup.hbs | 114 -------- .../admin/addon/components/ip-lookup.js | 120 -------- .../admin/addon/templates/user-index.hbs | 14 +- .../float-kit/addon/components/d-menu.gjs | 1 + .../stylesheets/common/admin/admin_base.scss | 111 ++++++-- .../stylesheets/common/admin/users.scss | 7 - config/locales/client.en.yml | 4 +- 8 files changed, 363 insertions(+), 274 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/ip-lookup.gjs delete mode 100644 app/assets/javascripts/admin/addon/components/ip-lookup.hbs delete mode 100644 app/assets/javascripts/admin/addon/components/ip-lookup.js diff --git a/app/assets/javascripts/admin/addon/components/ip-lookup.gjs b/app/assets/javascripts/admin/addon/components/ip-lookup.gjs new file mode 100644 index 00000000000..154550e91de --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/ip-lookup.gjs @@ -0,0 +1,266 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { LinkTo } from "@ember/routing"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner"; +import DButton from "discourse/components/d-button"; +import avatar from "discourse/helpers/avatar"; +import loadingSpinner from "discourse/helpers/loading-spinner"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { clipboardCopy } from "discourse/lib/utilities"; +import { i18n } from "discourse-i18n"; +import AdminUser from "admin/models/admin-user"; +import DMenu from "float-kit/components/d-menu"; + +export default class IpLookup extends Component { + @service dialog; + @service site; + @service toasts; + + @tracked location; + @tracked otherAccounts; + @tracked loading = false; + @tracked otherAccountsLoading = false; + @tracked totalOthersWithSameIP; + + get otherAccountsToDelete() { + const otherAccountsLength = this.otherAccounts?.length || 0; + const totalOthers = this.totalOthersWithSameIP || 0; + // can only delete up to 50 accounts at a time + const total = Math.min(50, totalOthers); + const visible = Math.min(50, otherAccountsLength); + return Math.max(visible, total); + } + + @action + async lookup() { + this.loading = true; + try { + if (!this.location && this.args.ip) { + const loc = await ajax("/admin/users/ip-info", { + data: { ip: this.args.ip }, + }); + this.location = loc; + } + + if (!this.otherAccounts && this.args.ip) { + this.otherAccountsLoading = true; + + const data = { + ip: this.args.ip, + exclude: this.args.userId, + order: "trust_level DESC", + }; + + const result = await ajax("/admin/users/total-others-with-same-ip", { + data, + }); + this.totalOthersWithSameIP = result.total; + + const users = await AdminUser.findAll("active", data); + this.otherAccounts = users; + this.otherAccountsLoading = false; + } + } catch (error) { + popupAjaxError(error); + } finally { + this.loading = false; + } + } + + @action + async copy() { + const { location } = this; + let text = `IP: ${this.args.ip}`; + + if (location) { + if (location.hostname) { + text += "\n" + `${i18n("ip_lookup.hostname")}: ${location.hostname}`; + } + + text += "\n" + i18n("ip_lookup.location"); + text += location.location + ? `: ${location.location}` + : `: ${i18n("ip_lookup.location_not_found")}`; + + if (location.organization) { + text += + "\n" + `${i18n("ip_lookup.organisation")}: ${location.organization}`; + } + } + + try { + await clipboardCopy(text.trim()); + this.toasts.success({ + duration: 3000, + data: { + message: i18n("ip_lookup.copied"), + }, + }); + } catch (err) { + popupAjaxError(err); + } + } + + @action + deleteOtherAccounts() { + this.dialog.yesNoConfirm({ + message: i18n("ip_lookup.confirm_delete_other_accounts"), + didConfirm: async () => { + // reset state + this.otherAccounts = null; + this.otherAccountsLoading = true; + this.totalOthersWithSameIP = null; + + try { + await ajax("/admin/users/delete-others-with-same-ip.json", { + type: "DELETE", + data: { + ip: this.args.ip, + exclude: this.args.userId, + order: "trust_level DESC", + }, + }); + } catch (err) { + popupAjaxError(err); + } + }, + }); + } + + @action + onRegisterApi(api) { + this.dMenu = api; + } + + @action + close() { + this.dMenu.close(); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/ip-lookup.hbs b/app/assets/javascripts/admin/addon/components/ip-lookup.hbs deleted file mode 100644 index bcae14a51e8..00000000000 --- a/app/assets/javascripts/admin/addon/components/ip-lookup.hbs +++ /dev/null @@ -1,114 +0,0 @@ -{{#if this.ip}} - -{{/if}} -{{#if this.show}} -
- {{d-icon - "xmark" - }} - {{#if this.copied}} - - {{else}} - - {{/if}} -

{{i18n "ip_lookup.title"}}

-

{{html-safe (i18n "ip_lookup.powered_by")}}

-
- {{#if this.location}} - {{#if this.location.hostname}} -
{{i18n "ip_lookup.hostname"}}
-
{{this.location.hostname}}
- {{/if}} - -
{{i18n "ip_lookup.location"}}
-
- {{#if this.location.location}} - - {{this.location.location}} - - {{else}} - {{i18n "ip_lookup.location_not_found"}} - {{/if}} -
- - {{#if this.location.organization}} -
{{i18n "ip_lookup.organisation"}}
-
{{this.location.organization}}
- {{/if}} - {{else}} - {{loading-spinner size="small"}} - {{/if}} - -
- {{i18n "ip_lookup.other_accounts"}} - {{this.totalOthersWithSameIP}} - {{#if this.other_accounts.length}} - - {{/if}} -
- - - {{#if this.other_accounts.length}} -
- - - - - - - - - - - - {{#each this.other_accounts as |a|}} - - - - - - - - {{/each}} - -
{{i18n "ip_lookup.username"}}{{i18n "ip_lookup.trust_level"}}{{i18n "ip_lookup.read_time"}}{{i18n "ip_lookup.topics_entered"}}{{i18n "ip_lookup.post_count"}}
- - {{avatar - a - usernamePath="user.username" - imageSize="small" - }} -   - {{a.username}} - - {{a.trustLevel.id}}{{a.time_read}}{{a.topics_entered}}{{a.post_count}}
-
- {{/if}} -
-
-
-{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/ip-lookup.js b/app/assets/javascripts/admin/addon/components/ip-lookup.js deleted file mode 100644 index 30fd6a48902..00000000000 --- a/app/assets/javascripts/admin/addon/components/ip-lookup.js +++ /dev/null @@ -1,120 +0,0 @@ -import Component from "@ember/component"; -import EmberObject, { action } from "@ember/object"; -import { service } from "@ember/service"; -import { classNames } from "@ember-decorators/component"; -import $ from "jquery"; -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; -import copyText from "discourse/lib/copy-text"; -import discourseLater from "discourse-common/lib/later"; -import discourseComputed from "discourse-common/utils/decorators"; -import { i18n } from "discourse-i18n"; -import AdminUser from "admin/models/admin-user"; - -@classNames("ip-lookup") -export default class IpLookup extends Component { - @service dialog; - - @discourseComputed("other_accounts.length", "totalOthersWithSameIP") - otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) { - // can only delete up to 50 accounts at a time - const total = Math.min(50, totalOthersWithSameIP || 0); - const visible = Math.min(50, otherAccountsLength || 0); - return Math.max(visible, total); - } - - @action - hide(event) { - event?.preventDefault(); - this.set("show", false); - } - - @action - lookup() { - this.set("show", true); - - if (!this.location) { - ajax("/admin/users/ip-info", { - data: { ip: this.ip }, - }).then((location) => this.set("location", EmberObject.create(location))); - } - - if (!this.other_accounts) { - this.set("otherAccountsLoading", true); - - const data = { - ip: this.ip, - exclude: this.userId, - order: "trust_level DESC", - }; - - ajax("/admin/users/total-others-with-same-ip", { - data, - }).then((result) => this.set("totalOthersWithSameIP", result.total)); - - AdminUser.findAll("active", data).then((users) => { - this.setProperties({ - other_accounts: users, - otherAccountsLoading: false, - }); - }); - } - } - - @action - copy() { - let text = `IP: ${this.ip}\n`; - const location = this.location; - if (location) { - if (location.hostname) { - text += `${i18n("ip_lookup.hostname")}: ${location.hostname}\n`; - } - - text += i18n("ip_lookup.location"); - if (location.location) { - text += `: ${location.location}\n`; - } else { - text += `: ${i18n("ip_lookup.location_not_found")}\n`; - } - - if (location.organization) { - text += i18n("ip_lookup.organisation"); - text += `: ${location.organization}\n`; - } - } - - const $copyRange = $('

'); - $copyRange.html(text.trim().replace(/\n/g, "
")); - $(document.body).append($copyRange); - if (copyText(text, $copyRange[0])) { - this.set("copied", true); - discourseLater(() => this.set("copied", false), 2000); - } - $copyRange.remove(); - } - - @action - deleteOtherAccounts() { - this.dialog.yesNoConfirm({ - message: i18n("ip_lookup.confirm_delete_other_accounts"), - didConfirm: () => { - this.setProperties({ - other_accounts: null, - otherAccountsLoading: true, - totalOthersWithSameIP: null, - }); - - ajax("/admin/users/delete-others-with-same-ip.json", { - type: "DELETE", - data: { - ip: this.ip, - exclude: this.userId, - order: "trust_level DESC", - }, - }) - .catch(popupAjaxError) - .finally(this.send("lookup")); - }, - }); - } -} diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs index a9daf9b951a..bfffbd524f1 100644 --- a/app/assets/javascripts/admin/addon/templates/user-index.hbs +++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs @@ -193,7 +193,9 @@
{{this.model.ip_address}}
{{#if this.currentUser.staff}} - + {{#if this.model.ip_address}} + + {{/if}} {{/if}}
@@ -203,10 +205,12 @@
{{this.model.registration_ip_address}}
{{#if this.currentUser.staff}} - + {{#if this.model.registration_ip_address}} + + {{/if}} {{/if}}
diff --git a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs index 77861063115..7f64042215c 100644 --- a/app/assets/javascripts/float-kit/addon/components/d-menu.gjs +++ b/app/assets/javascripts/float-kit/addon/components/d-menu.gjs @@ -97,6 +97,7 @@ export default class DMenu extends Component { @translatedLabel={{@label}} @translatedTitle={{@title}} @disabled={{@disabled}} + @isLoading={{@isLoading}} aria-expanded={{if this.menuInstance.expanded "true" "false"}} {{on "keydown" this.forwardTabToContent}} ...attributes diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 8bde1ca1a9a..605c17e4bf9 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -425,37 +425,96 @@ $mobile-breakpoint: 700px; } } -.ip-lookup { - position: relative; - display: inline-block; +.ip-lookup-content { .location-box { - position: absolute; - width: 460px; - right: 0; - z-index: z("dropdown"); - box-shadow: var(--shadow-card); - margin-top: -2px; - background-color: var(--secondary); - padding: 12px 12px 5px; + padding: 1em; + max-width: 100%; + box-sizing: border-box; + + .title { + font-weight: bold; + font-size: var(--font-up-1); + display: flex; + align-items: center; + } + + &__controls { + display: flex; + margin-left: auto; + + align-items: center; + } + .powered-by { font-size: var(--font-down-1); - position: absolute; - bottom: -10px; - left: 10px; + color: var(--primary-high); } - .other-accounts { - margin: 5px 0 0; - max-height: 200px; - overflow: auto; - width: 455px; - ul { - margin: 0; + + .loading-container { + max-width: 100%; + } + + dl { + margin-bottom: 0; + } + + dt { + font-weight: bold; + &.other-accounts { + display: flex; + align-items: center; + font-weight: normal; + font-size: var(--font-down-1); + + .btn { + margin-left: auto; + } + .count { + font-weight: bold; + margin-left: 0.25em; + color: var(--danger); + } } - li { - list-style: none; - } - tr td:first-of-type { - width: 130px; + } + + dd { + margin: 0.25em 0 1em; + &.other-accounts { + margin: 1em 0 0 0; + max-height: 13em; + overflow: auto; + font-size: var(--font-down-1); + padding-right: 0.25em; + + thead { + position: sticky; + top: 0; + line-height: 1.2; + background: var(--secondary); + th { + padding: 0.25em; + } + } + + ul { + margin: 0; + } + li { + list-style: none; + } + + td { + padding: 0.25em 0.25em 0.25em 0; + } + + td.user { + white-space: nowrap; + + img { + width: 1.25em; + height: 1.25em; + } + } } } } diff --git a/app/assets/stylesheets/common/admin/users.scss b/app/assets/stylesheets/common/admin/users.scss index 3e794859c42..8400210bcee 100644 --- a/app/assets/stylesheets/common/admin/users.scss +++ b/app/assets/stylesheets/common/admin/users.scss @@ -210,11 +210,4 @@ .associations button { margin: 5px 5px 0 0; } - - .ip-lookup { - display: block; - .location-box { - left: 0; - } - } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c75fd068a25..d5b2ee2d68d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1207,10 +1207,10 @@ en: one: "Delete %{count}" other: "Delete %{count}" username: "username" - trust_level: "TL" + trust_level: "trust level" read_time: "read time" topics_entered: "topics entered" - post_count: "# posts" + post_count: "posts" confirm_delete_other_accounts: "Are you sure you want to delete these accounts?" powered_by: "using MaxMindDB" copied: "copied"