FEATURE: Add pagination to API keys page (#14777)
This commit is contained in:
parent
42f65b4c48
commit
b203e316ac
|
@ -1,14 +1,39 @@
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
|
import { action } from "@ember/object";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
actions: {
|
loading: false,
|
||||||
revokeKey(key) {
|
|
||||||
key.revoke().catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
|
|
||||||
undoRevokeKey(key) {
|
@action
|
||||||
key.undoRevoke().catch(popupAjaxError);
|
revokeKey(key) {
|
||||||
},
|
key.revoke().catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
undoRevokeKey(key) {
|
||||||
|
key.undoRevoke().catch(popupAjaxError);
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
loadMore() {
|
||||||
|
if (this.loading || this.model.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = 50;
|
||||||
|
|
||||||
|
this.set("loading", true);
|
||||||
|
this.store
|
||||||
|
.findAll("api-key", { offset: this.model.length, limit })
|
||||||
|
.then((keys) => {
|
||||||
|
this.model.addObjects(keys);
|
||||||
|
if (keys.length < limit) {
|
||||||
|
this.model.set("loaded", true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.set("loading", false);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,67 +5,71 @@
|
||||||
label="admin.api.new_key"}}
|
label="admin.api.new_key"}}
|
||||||
|
|
||||||
{{#if model}}
|
{{#if model}}
|
||||||
<table class="api-keys grid">
|
{{#load-more selector=".api-keys tr" action=(action "loadMore")}}
|
||||||
<thead>
|
<table class="api-keys grid">
|
||||||
<th>{{i18n "admin.api.key"}}</th>
|
<thead>
|
||||||
<th>{{i18n "admin.api.description"}}</th>
|
<th>{{i18n "admin.api.key"}}</th>
|
||||||
<th>{{i18n "admin.api.user"}}</th>
|
<th>{{i18n "admin.api.description"}}</th>
|
||||||
<th>{{i18n "admin.api.created"}}</th>
|
<th>{{i18n "admin.api.user"}}</th>
|
||||||
<th>{{i18n "admin.api.last_used"}}</th>
|
<th>{{i18n "admin.api.created"}}</th>
|
||||||
<th> </th>
|
<th>{{i18n "admin.api.last_used"}}</th>
|
||||||
</thead>
|
<th> </th>
|
||||||
<tbody>
|
</thead>
|
||||||
{{#each model as |k|}}
|
<tbody>
|
||||||
<tr class={{if k.revoked_at "revoked"}}>
|
{{#each model as |k|}}
|
||||||
<td class="key">
|
<tr class={{if k.revoked_at "revoked"}}>
|
||||||
{{#if k.revoked_at}}{{d-icon "times-circle"}}{{/if}}
|
<td class="key">
|
||||||
{{k.truncatedKey}}
|
{{#if k.revoked_at}}{{d-icon "times-circle"}}{{/if}}
|
||||||
</td>
|
{{k.truncatedKey}}
|
||||||
<td class="key-description">
|
</td>
|
||||||
{{k.shortDescription}}
|
<td class="key-description">
|
||||||
</td>
|
{{k.shortDescription}}
|
||||||
<td class="key-user">
|
</td>
|
||||||
<div class="label">{{i18n "admin.api.user"}}</div>
|
<td class="key-user">
|
||||||
{{#if k.user}}
|
<div class="label">{{i18n "admin.api.user"}}</div>
|
||||||
{{#link-to "adminUser" k.user}}
|
{{#if k.user}}
|
||||||
{{avatar k.user imageSize="small"}}
|
{{#link-to "adminUser" k.user}}
|
||||||
{{/link-to}}
|
{{avatar k.user imageSize="small"}}
|
||||||
{{else}}
|
{{/link-to}}
|
||||||
{{i18n "admin.api.all_users"}}
|
{{else}}
|
||||||
{{/if}}
|
{{i18n "admin.api.all_users"}}
|
||||||
</td>
|
{{/if}}
|
||||||
<td class="key-created">
|
</td>
|
||||||
<div class="label">{{i18n "admin.api.created"}}</div>
|
<td class="key-created">
|
||||||
{{format-date k.created_at}}
|
<div class="label">{{i18n "admin.api.created"}}</div>
|
||||||
</td>
|
{{format-date k.created_at}}
|
||||||
<td class="key-last-used">
|
</td>
|
||||||
<div class="label">{{i18n "admin.api.last_used"}}</div>
|
<td class="key-last-used">
|
||||||
{{#if k.last_used_at}}
|
<div class="label">{{i18n "admin.api.last_used"}}</div>
|
||||||
{{format-date k.last_used_at}}
|
{{#if k.last_used_at}}
|
||||||
{{else}}
|
{{format-date k.last_used_at}}
|
||||||
{{i18n "admin.api.never_used"}}
|
{{else}}
|
||||||
{{/if}}
|
{{i18n "admin.api.never_used"}}
|
||||||
</td>
|
{{/if}}
|
||||||
<td class="key-controls">
|
</td>
|
||||||
{{d-button action=(route-action "show" k) icon="far-eye" title="admin.api.show_details"}}
|
<td class="key-controls">
|
||||||
{{#if k.revoked_at}}
|
{{d-button action=(route-action "show" k) icon="far-eye" title="admin.api.show_details"}}
|
||||||
{{d-button
|
{{#if k.revoked_at}}
|
||||||
action=(action "undoRevokeKey")
|
{{d-button
|
||||||
actionParam=k icon="undo"
|
action=(action "undoRevokeKey")
|
||||||
title="admin.api.undo_revoke"}}
|
actionParam=k icon="undo"
|
||||||
{{else}}
|
title="admin.api.undo_revoke"}}
|
||||||
{{d-button
|
{{else}}
|
||||||
class="btn-danger"
|
{{d-button
|
||||||
action=(action "revokeKey")
|
class="btn-danger"
|
||||||
actionParam=k
|
action=(action "revokeKey")
|
||||||
icon="times"
|
actionParam=k
|
||||||
title="admin.api.revoke"}}
|
icon="times"
|
||||||
{{/if}}
|
title="admin.api.revoke"}}
|
||||||
</td>
|
{{/if}}
|
||||||
</tr>
|
</td>
|
||||||
{{/each}}
|
</tr>
|
||||||
</tbody>
|
{{/each}}
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{/load-more}}
|
||||||
|
|
||||||
|
{{conditional-loading-spinner condition=loading}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>{{i18n "admin.api.none"}}</p>
|
<p>{{i18n "admin.api.none"}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -5,18 +5,22 @@ class Admin::ApiController < Admin::AdminController
|
||||||
# If we used "api_key", then our user provider would try to use the value for authentication
|
# If we used "api_key", then our user provider would try to use the value for authentication
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
offset = (params[:offset] || 0).to_i
|
||||||
|
limit = (params[:limit] || 50).to_i.clamp(1, 50)
|
||||||
|
|
||||||
keys = ApiKey
|
keys = ApiKey
|
||||||
.where(hidden: false)
|
.where(hidden: false)
|
||||||
.includes(:user, :api_key_scopes)
|
.includes(:user, :api_key_scopes)
|
||||||
|
# Sort revoked keys by revoked_at and active keys by created_at
|
||||||
|
.order("revoked_at DESC NULLS FIRST, created_at DESC")
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
|
||||||
# Put active keys first
|
render_json_dump(
|
||||||
# Sort active keys by created_at, sort revoked keys by revoked_at
|
keys: serialize_data(keys, ApiKeySerializer),
|
||||||
keys = keys.order(<<~SQL)
|
offset: offset,
|
||||||
CASE WHEN revoked_at IS NULL THEN 0 ELSE 1 END,
|
limit: limit
|
||||||
COALESCE(revoked_at, created_at) DESC
|
)
|
||||||
SQL
|
|
||||||
|
|
||||||
render_serialized(keys.to_a, ApiKeySerializer, root: 'keys')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
@ -12,6 +12,7 @@ describe Admin::ApiController do
|
||||||
|
|
||||||
fab!(:key1, refind: false) { Fabricate(:api_key, description: "my key") }
|
fab!(:key1, refind: false) { Fabricate(:api_key, description: "my key") }
|
||||||
fab!(:key2, refind: false) { Fabricate(:api_key, user: admin) }
|
fab!(:key2, refind: false) { Fabricate(:api_key, user: admin) }
|
||||||
|
fab!(:key3, refind: false) { Fabricate(:api_key, user: admin) }
|
||||||
|
|
||||||
context "as an admin" do
|
context "as an admin" do
|
||||||
before do
|
before do
|
||||||
|
@ -22,7 +23,21 @@ describe Admin::ApiController do
|
||||||
it "succeeds" do
|
it "succeeds" do
|
||||||
get "/admin/api/keys.json"
|
get "/admin/api/keys.json"
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(response.parsed_body["keys"].length).to eq(2)
|
expect(response.parsed_body["keys"].length).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can paginate results" do
|
||||||
|
get "/admin/api/keys.json?offset=0&limit=2"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key3.id, key2.id)
|
||||||
|
|
||||||
|
get "/admin/api/keys.json?offset=1&limit=2"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key2.id, key1.id)
|
||||||
|
|
||||||
|
get "/admin/api/keys.json?offset=2&limit=2"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(key1.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -246,7 +261,7 @@ describe Admin::ApiController do
|
||||||
}
|
}
|
||||||
expect(response.status).to eq(404)
|
expect(response.status).to eq(404)
|
||||||
|
|
||||||
expect(ApiKey.count).to eq(2)
|
expect(ApiKey.count).to eq(3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue