UX: refactor IP lookup using DMenu to improve layout and positioning (#30374)
This commit is contained in:
parent
6873962572
commit
2a3f0f3bef
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DMenu
|
||||||
|
@identifier="ip-lookup"
|
||||||
|
@label={{i18n "admin.user.ip_lookup"}}
|
||||||
|
@icon="globe"
|
||||||
|
@onShow={{this.lookup}}
|
||||||
|
@modalForMobile={{true}}
|
||||||
|
@onRegisterApi={{this.onRegisterApi}}
|
||||||
|
@isLoading={{this.loading}}
|
||||||
|
>
|
||||||
|
<:content>
|
||||||
|
<div class="location-box">
|
||||||
|
<div class="location-box__content">
|
||||||
|
<div class="title">
|
||||||
|
{{i18n "ip_lookup.title"}}
|
||||||
|
<div class="location-box__controls">
|
||||||
|
<DButton
|
||||||
|
@action={{this.copy}}
|
||||||
|
@icon="copy"
|
||||||
|
class="btn-transparent"
|
||||||
|
/>
|
||||||
|
{{#if this.site.mobileView}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.close}}
|
||||||
|
@icon="xmark"
|
||||||
|
class="btn-transparent"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
{{#if this.location}}
|
||||||
|
{{#if this.location.hostname}}
|
||||||
|
<dt>{{i18n "ip_lookup.hostname"}}</dt>
|
||||||
|
<dd>{{this.location.hostname}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<dt>{{i18n "ip_lookup.location"}}</dt>
|
||||||
|
<dd>
|
||||||
|
{{#if this.location.location}}
|
||||||
|
<a
|
||||||
|
href="https://maps.google.com/maps?q={{this.location.latitude}},{{this.location.longitude}}"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{this.location.location}}
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
{{i18n "ip_lookup.location_not_found"}}
|
||||||
|
{{/if}}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
{{#if this.location.organization}}
|
||||||
|
<dt>{{i18n "ip_lookup.organisation"}}</dt>
|
||||||
|
<dd>{{this.location.organization}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{loadingSpinner size="small"}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<dt class="other-accounts">
|
||||||
|
{{i18n "ip_lookup.other_accounts"}}
|
||||||
|
<span class="count">{{this.totalOthersWithSameIP}}</span>
|
||||||
|
{{#if this.otherAccounts}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.deleteOtherAccounts}}
|
||||||
|
@icon="triangle-exclamation"
|
||||||
|
@translatedLabel={{i18n
|
||||||
|
"ip_lookup.delete_other_accounts"
|
||||||
|
count=this.otherAccountsToDelete
|
||||||
|
}}
|
||||||
|
class="btn-danger pull-right"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</dt>
|
||||||
|
|
||||||
|
<ConditionalLoadingSpinner
|
||||||
|
@size="small"
|
||||||
|
@condition={{this.otherAccountsLoading}}
|
||||||
|
>
|
||||||
|
{{#if this.otherAccounts}}
|
||||||
|
<dd class="other-accounts">
|
||||||
|
<table class="table table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{i18n "ip_lookup.username"}}</th>
|
||||||
|
<th>{{i18n "ip_lookup.trust_level"}}</th>
|
||||||
|
<th>{{i18n "ip_lookup.read_time"}}</th>
|
||||||
|
<th>{{i18n "ip_lookup.topics_entered"}}</th>
|
||||||
|
<th>{{i18n "ip_lookup.post_count"}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each this.otherAccounts as |account|}}
|
||||||
|
<tr>
|
||||||
|
<td class="user">
|
||||||
|
<LinkTo @route="adminUser" @model={{account}}>
|
||||||
|
{{avatar account imageSize="tiny"}}
|
||||||
|
<span>{{account.username}}</span>
|
||||||
|
</LinkTo>
|
||||||
|
</td>
|
||||||
|
<td>{{account.trustLevel.id}}</td>
|
||||||
|
<td>{{account.time_read}}</td>
|
||||||
|
<td>{{account.topics_entered}}</td>
|
||||||
|
<td>{{account.post_count}}</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</dd>
|
||||||
|
{{/if}}
|
||||||
|
</ConditionalLoadingSpinner>
|
||||||
|
</dl>
|
||||||
|
<div class="powered-by">{{htmlSafe
|
||||||
|
(i18n "ip_lookup.powered_by")
|
||||||
|
}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</:content>
|
||||||
|
</DMenu>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,114 +0,0 @@
|
||||||
{{#if this.ip}}
|
|
||||||
<DButton
|
|
||||||
@action={{this.lookup}}
|
|
||||||
@icon="globe"
|
|
||||||
@label="admin.user.ip_lookup"
|
|
||||||
class="btn-default"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{#if this.show}}
|
|
||||||
<div class="location-box">
|
|
||||||
<a href class="close pull-right" {{on "click" this.hide}}>{{d-icon
|
|
||||||
"xmark"
|
|
||||||
}}</a>
|
|
||||||
{{#if this.copied}}
|
|
||||||
<DButton
|
|
||||||
@icon="copy"
|
|
||||||
@label="ip_lookup.copied"
|
|
||||||
class="btn-hover pull-right"
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<DButton @action={{this.copy}} @icon="copy" class="pull-right no-text" />
|
|
||||||
{{/if}}
|
|
||||||
<h4>{{i18n "ip_lookup.title"}}</h4>
|
|
||||||
<p class="powered-by">{{html-safe (i18n "ip_lookup.powered_by")}}</p>
|
|
||||||
<dl>
|
|
||||||
{{#if this.location}}
|
|
||||||
{{#if this.location.hostname}}
|
|
||||||
<dt>{{i18n "ip_lookup.hostname"}}</dt>
|
|
||||||
<dd>{{this.location.hostname}}</dd>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<dt>{{i18n "ip_lookup.location"}}</dt>
|
|
||||||
<dd>
|
|
||||||
{{#if this.location.location}}
|
|
||||||
<a
|
|
||||||
href="https://maps.google.com/maps?q={{this.location.latitude}},{{this.location.longitude}}"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{this.location.location}}
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
{{i18n "ip_lookup.location_not_found"}}
|
|
||||||
{{/if}}
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
{{#if this.location.organization}}
|
|
||||||
<dt>{{i18n "ip_lookup.organisation"}}</dt>
|
|
||||||
<dd>{{this.location.organization}}</dd>
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{loading-spinner size="small"}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<dt>
|
|
||||||
{{i18n "ip_lookup.other_accounts"}}
|
|
||||||
<strong>{{this.totalOthersWithSameIP}}</strong>
|
|
||||||
{{#if this.other_accounts.length}}
|
|
||||||
<DButton
|
|
||||||
@action={{this.deleteOtherAccounts}}
|
|
||||||
@icon="triangle-exclamation"
|
|
||||||
@translatedLabel={{i18n
|
|
||||||
"ip_lookup.delete_other_accounts"
|
|
||||||
count=this.otherAccountsToDelete
|
|
||||||
}}
|
|
||||||
class="btn-danger pull-right"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</dt>
|
|
||||||
|
|
||||||
<ConditionalLoadingSpinner
|
|
||||||
@size="small"
|
|
||||||
@condition={{this.otherAccountsLoading}}
|
|
||||||
>
|
|
||||||
{{#if this.other_accounts.length}}
|
|
||||||
<dd class="other-accounts">
|
|
||||||
<table class="table table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{i18n "ip_lookup.username"}}</th>
|
|
||||||
<th>{{i18n "ip_lookup.trust_level"}}</th>
|
|
||||||
<th>{{i18n "ip_lookup.read_time"}}</th>
|
|
||||||
<th>{{i18n "ip_lookup.topics_entered"}}</th>
|
|
||||||
<th>{{i18n "ip_lookup.post_count"}}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each this.other_accounts as |a|}}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<LinkTo @route="adminUser" @model={{a}}>
|
|
||||||
{{avatar
|
|
||||||
a
|
|
||||||
usernamePath="user.username"
|
|
||||||
imageSize="small"
|
|
||||||
}}
|
|
||||||
|
|
||||||
<span>{{a.username}}</span>
|
|
||||||
</LinkTo>
|
|
||||||
</td>
|
|
||||||
<td>{{a.trustLevel.id}}</td>
|
|
||||||
<td>{{a.time_read}}</td>
|
|
||||||
<td>{{a.topics_entered}}</td>
|
|
||||||
<td>{{a.post_count}}</td>
|
|
||||||
</tr>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</dd>
|
|
||||||
{{/if}}
|
|
||||||
</ConditionalLoadingSpinner>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
|
@ -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 = $('<p id="copy-range"></p>');
|
|
||||||
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
|
|
||||||
$(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"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -193,7 +193,9 @@
|
||||||
<div class="value">{{this.model.ip_address}}</div>
|
<div class="value">{{this.model.ip_address}}</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{#if this.currentUser.staff}}
|
{{#if this.currentUser.staff}}
|
||||||
<IpLookup @ip={{this.model.ip_address}} @userId={{this.model.id}} />
|
{{#if this.model.ip_address}}
|
||||||
|
<IpLookup @ip={{this.model.ip_address}} @userId={{this.model.id}} />
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,10 +205,12 @@
|
||||||
<div class="value">{{this.model.registration_ip_address}}</div>
|
<div class="value">{{this.model.registration_ip_address}}</div>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
{{#if this.currentUser.staff}}
|
{{#if this.currentUser.staff}}
|
||||||
<IpLookup
|
{{#if this.model.registration_ip_address}}
|
||||||
@ip={{this.model.registration_ip_address}}
|
<IpLookup
|
||||||
@userId={{this.model.id}}
|
@ip={{this.model.registration_ip_address}}
|
||||||
/>
|
@userId={{this.model.id}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -97,6 +97,7 @@ export default class DMenu extends Component {
|
||||||
@translatedLabel={{@label}}
|
@translatedLabel={{@label}}
|
||||||
@translatedTitle={{@title}}
|
@translatedTitle={{@title}}
|
||||||
@disabled={{@disabled}}
|
@disabled={{@disabled}}
|
||||||
|
@isLoading={{@isLoading}}
|
||||||
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
aria-expanded={{if this.menuInstance.expanded "true" "false"}}
|
||||||
{{on "keydown" this.forwardTabToContent}}
|
{{on "keydown" this.forwardTabToContent}}
|
||||||
...attributes
|
...attributes
|
||||||
|
|
|
@ -425,37 +425,96 @@ $mobile-breakpoint: 700px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-lookup {
|
.ip-lookup-content {
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
.location-box {
|
.location-box {
|
||||||
position: absolute;
|
padding: 1em;
|
||||||
width: 460px;
|
max-width: 100%;
|
||||||
right: 0;
|
box-sizing: border-box;
|
||||||
z-index: z("dropdown");
|
|
||||||
box-shadow: var(--shadow-card);
|
.title {
|
||||||
margin-top: -2px;
|
font-weight: bold;
|
||||||
background-color: var(--secondary);
|
font-size: var(--font-up-1);
|
||||||
padding: 12px 12px 5px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__controls {
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.powered-by {
|
.powered-by {
|
||||||
font-size: var(--font-down-1);
|
font-size: var(--font-down-1);
|
||||||
position: absolute;
|
color: var(--primary-high);
|
||||||
bottom: -10px;
|
|
||||||
left: 10px;
|
|
||||||
}
|
}
|
||||||
.other-accounts {
|
|
||||||
margin: 5px 0 0;
|
.loading-container {
|
||||||
max-height: 200px;
|
max-width: 100%;
|
||||||
overflow: auto;
|
}
|
||||||
width: 455px;
|
|
||||||
ul {
|
dl {
|
||||||
margin: 0;
|
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;
|
|
||||||
}
|
dd {
|
||||||
tr td:first-of-type {
|
margin: 0.25em 0 1em;
|
||||||
width: 130px;
|
&.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,11 +210,4 @@
|
||||||
.associations button {
|
.associations button {
|
||||||
margin: 5px 5px 0 0;
|
margin: 5px 5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ip-lookup {
|
|
||||||
display: block;
|
|
||||||
.location-box {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1207,10 +1207,10 @@ en:
|
||||||
one: "Delete %{count}"
|
one: "Delete %{count}"
|
||||||
other: "Delete %{count}"
|
other: "Delete %{count}"
|
||||||
username: "username"
|
username: "username"
|
||||||
trust_level: "TL"
|
trust_level: "trust level"
|
||||||
read_time: "read time"
|
read_time: "read time"
|
||||||
topics_entered: "topics entered"
|
topics_entered: "topics entered"
|
||||||
post_count: "# posts"
|
post_count: "posts"
|
||||||
confirm_delete_other_accounts: "Are you sure you want to delete these accounts?"
|
confirm_delete_other_accounts: "Are you sure you want to delete these accounts?"
|
||||||
powered_by: "using <a href='https://maxmind.com'>MaxMindDB</a>"
|
powered_by: "using <a href='https://maxmind.com'>MaxMindDB</a>"
|
||||||
copied: "copied"
|
copied: "copied"
|
||||||
|
|
Loading…
Reference in New Issue