Skip to content
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

Added pagination and filter support for subscriptions in the test console #2516

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,52 @@ <h3 class="pt-0">
</div>
<div class="col-7">
<div class="form-group">
<!-- ko if: $component.products() && $component.products().length > 0 -->
<select id="subscriptionKey" class="form-control" data-bind="value: $component.selectedSubscriptionKey">
<!-- ko foreach: { data: $component.products, as: 'product' } -->
<optgroup data-bind="attr: { label: product.name }">
<!-- ko foreach: { data: product.subscriptionKeys, as: 'subscriptionKey' } -->
<option data-bind="value: subscriptionKey.value, text: subscriptionKey.name"></option>
<!-- ko if: ($component.products() && $component.products().length > 0) || isSubscriptionListEmptyDueToFilter() -->
<div class="input-group" tabindex="0" aria-label="Subscriptions">
<div class="form-control text-truncate" data-toggle="dropdown" role="button">
<!-- ko if: $component.selectedSubscriptionKey() -->
<span data-bind="text: $component.selectedSubscriptionKey().name"></span>
<!-- /ko -->
</optgroup>
<!-- /ko -->
</select>
</div>
<button class="input-group-addon no-border" data-toggle="dropdown"
aria-label="Expand subscription list">
<i class="icon-emb icon-emb-chevron-down"></i>
</button>
<div class="dropdown" id="subscriptions-dropdown">
<!-- ko if: $component.subscriptionsLoading -->
<spinner class="block" role="presentation"></spinner>
<!-- /ko -->
<!-- ko ifnot: $component.subscriptionsLoading -->
<input type="search" class="form-control form-control-light" aria-label="Search"
placeholder="Search subscriptions" data-bind="textInput: subscriptionsPattern" autofocus />
<!-- ko if: isSubscriptionListEmptyDueToFilter() -->
<span>No subscriptions found.</span>
<!-- /ko -->
<!-- ko foreach: { data: $component.products, as: 'product' } -->
<span data-bind="text: product.name" style="font-weight: bold"></span>
<div class="menu menu-vertical" role="list">
<!-- ko foreach: { data: product.subscriptionKeys, as: 'item' } -->
<a href="#" role="listitem" class="nav-link text-truncate" data-dismiss
data-bind="click: $component.selectSubscription">
<span data-bind="text: item.name"></span>
</a>
<!-- /ko -->
</div>
<!-- /ko -->
<!-- ko if: $component.nextSubscriptionsPage() || $component.subscriptionsPageNumber() > 1 -->
<pagination
params="{ pageNumber: $component.subscriptionsPageNumber, nextPage: $component.nextSubscriptionsPage }">
</pagination>
<!-- /ko -->
<!-- /ko -->
</div>
</div>

<!-- /ko -->
<!-- ko if: !$component.products() || $component.products().length === 0 -->
<!-- ko if: (!$component.products() || $component.products().length === 0) && !isSubscriptionListEmptyDueToFilter() -->
<div class="input-group">
<input id="subscriptionKey" class="form-control" placeholder="subscription key"
data-bind="textInput: $component.selectedSubscriptionKey, attr: { type: subscriptionKeyRevealed() ? 'text' : 'password' }"
data-bind="textInput: $component.selectedSubscriptionKey.value, attr: { type: subscriptionKeyRevealed() ? 'text' : 'password' }"
aria-required="true" />
<button data-bind="click: toggleSubscriptionKey" class="input-group-addon">
<i
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ import { ProductService } from "../../../../../services/productService";
import { SubscriptionState } from "../../../../../contracts/subscription";
import { ConsoleParameter } from "../../../../../models/console/consoleParameter";
import { OAuth2AuthenticationSettings } from "../../../../../contracts/authenticationSettings";
import * as Constants from "../../../../../constants";
import { SearchQuery } from "../../../../../contracts/searchQuery";

interface SubscriptionOption {
name: string;
value: string;
}

const maxSubscriptionsPageNumber = 5;

@Component({
selector: "authorization",
Expand All @@ -35,10 +44,16 @@ export class Authorization {
public readonly password: ko.Observable<string>;
public readonly authorizationError: ko.Observable<string>;
public readonly products: ko.Observable<Product[]>;
public readonly selectedSubscriptionKey: ko.Observable<string>;
public readonly selectedSubscriptionKey: ko.Observable<SubscriptionOption>;
public readonly subscriptionKeyRevealed: ko.Observable<boolean>;
private deleteAuthorizationHeader: boolean = false;
public readonly selectedAuthorizationServer: ko.Observable<AuthorizationServer>;
public readonly subscriptionsPattern: ko.Observable<string>;
public readonly subscriptionSelection: ko.Computed<string>;
public readonly subscriptionsPageNumber: ko.Observable<number>;
public readonly nextSubscriptionsPage: ko.Observable<boolean>;
public readonly isSubscriptionListEmptyDueToFilter: ko.Observable<boolean>;
public readonly subscriptionsLoading = ko.observable<boolean>(false);

constructor(
private readonly sessionManager: SessionManager,
Expand All @@ -63,6 +78,14 @@ export class Authorization {
this.subscriptionKeyRevealed = ko.observable(false);
this.authorizationServers = ko.observable<AuthorizationServer[]>();
this.selectedAuthorizationServer = ko.observable<AuthorizationServer>();
this.subscriptionsPattern = ko.observable();
this.subscriptionsPageNumber = ko.observable(1);
this.nextSubscriptionsPage = ko.observable();
this.isSubscriptionListEmptyDueToFilter = ko.observable(false);
this.subscriptionsLoading = ko.observable(true);
this.subscriptionSelection = ko.computed(() => {
return this.selectedSubscriptionKey() ? this.selectedSubscriptionKey().name : "Select a subscription";
});
}
@Param()
public authorizationServers: ko.Observable<AuthorizationServer[]>;
Expand Down Expand Up @@ -102,8 +125,18 @@ export class Authorization {

await this.setupOAuth();
if (this.api().subscriptionRequired) {
await this.loadSubscriptionKeys();
await this.loadSubscriptionKeys(true);
}

this.subscriptionsPattern
.extend({ rateLimit: { timeout: Constants.defaultInputDelayMs, method: "notifyWhenChangesStop" } })
.subscribe(this.resetSubscriptionsSearch);
this.subscriptionsPageNumber.subscribe(() => this.loadSubscriptionKeys());
}

public async resetSubscriptionsSearch(): Promise<void> {
this.subscriptionsPageNumber(1);
this.loadSubscriptionKeys();
}

public toggleSubscriptionKey(): void {
Expand Down Expand Up @@ -135,6 +168,11 @@ export class Authorization {
}
}

public selectSubscription(subscription: SubscriptionOption) {
this.selectedSubscriptionKey(subscription);
this.closeDropdown();
}

private async getStoredCredentials(serverName: string, scopeOverride: string): Promise<StoredCredentials> {
const oauthSession = await this.sessionManager.getItem<OAuthSession>(oauthSessionKey);
const recordKey = this.getSessionRecordKey(serverName, scopeOverride);
Expand Down Expand Up @@ -367,21 +405,35 @@ export class Authorization {
}
}

private async loadSubscriptionKeys(): Promise<void> {
private async loadSubscriptionKeys(selectFirstSubscription: boolean = false): Promise<void> {
this.subscriptionsLoading(true);
const userId = await this.usersService.getCurrentUserId();

if (!userId) {
return;
}

const pageNumber = this.subscriptionsPageNumber() - 1;
const subscriptionsQuery: SearchQuery = {
pattern: this.subscriptionsPattern(),
skip: pageNumber * maxSubscriptionsPageNumber,
take: maxSubscriptionsPageNumber
};

const pageOfProducts = await this.apiService.getAllApiProducts(this.api().id);
const products = pageOfProducts && pageOfProducts.value ? pageOfProducts.value : [];
const pageOfSubscriptions = await this.productService.getSubscriptions(userId);
const pageOfSubscriptions = await this.productService.getSubscriptions(userId, null, subscriptionsQuery);
const subscriptions = pageOfSubscriptions.value.filter(subscription => subscription.state === SubscriptionState.active);
const availableProducts = [];

this.nextSubscriptionsPage(!!pageOfSubscriptions.nextLink);

products.forEach(product => {
const keys = [];
const keys: SubscriptionOption[] = [];

if (subscriptions.length === 0) {
return;
}

subscriptions.forEach(subscription => {
if (!this.productService.isScopeSuitable(subscription.scope, this.api().name, product.name)) {
Expand All @@ -404,24 +456,30 @@ export class Authorization {
}
});

this.isSubscriptionListEmptyDueToFilter(availableProducts.length == 0 && this.subscriptionsPattern() !== undefined);
this.products(availableProducts);
this.subscriptionsLoading(false);

if (subscriptions.length == 0) {
return;
}

if (availableProducts.length > 0) {
const subscriptionKey = availableProducts[0].subscriptionKeys[0].value;
if (availableProducts.length > 0 && selectFirstSubscription) {
const subscriptionKey = availableProducts[0].subscriptionKeys[0];
this.selectedSubscriptionKey(subscriptionKey);
this.applySubscriptionKey(subscriptionKey);
}
}

private applySubscriptionKey(subscriptionKey: string): void {
private applySubscriptionKey(subscriptionKey: SubscriptionOption): void {
if (!this.consoleOperation() && !this.isGraphQL()) {
return;
}

if (this.api().type === TypeOfApi.webSocket || this.isGraphQL()) {
this.setSubscriptionKeyParameter(subscriptionKey);
this.setSubscriptionKeyParameter(subscriptionKey.value);
} else {
this.setSubscriptionKeyHeader(subscriptionKey);
this.setSubscriptionKeyHeader(subscriptionKey.value);
}

if (!this.isGraphQL()) {
Expand Down Expand Up @@ -488,4 +546,13 @@ export class Authorization {
}
return null;
}

private closeDropdown(): true {
const subscriptionDropdowm = document.getElementById("subscriptions-dropdown");
if (subscriptionDropdowm.classList.contains("show")) {
subscriptionDropdowm.classList.remove("show");
}
// return true to not-prevent the default action https://knockoutjs.com/documentation/click-binding.html#note-3-allowing-the-default-click-action
return true;
}
}
20 changes: 18 additions & 2 deletions src/services/productService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,29 @@ export class ProductService {
* @param userId {string} User unique identifier.
* @param productId {string} Product unique identifier.
*/
public async getSubscriptions(userId: string, productId?: string): Promise<Page<Subscription>> {
public async getSubscriptions(userId: string, productId?: string, searchRequest?: SearchQuery): Promise<Page<Subscription>> {
if (!userId) {
throw new Error(`Parameter "userId" not specified.`);
}

const skip = searchRequest && searchRequest.skip || 0;
const take = searchRequest && searchRequest.take || Constants.defaultPageSize;
const odataFilterEntries = [];
if (productId) {
odataFilterEntries.push(`properties/scope eq '${productId}'`)
}

if (searchRequest?.pattern) {
const pattern = Utils.encodeURICustomized(searchRequest.pattern, Constants.reservedCharTuplesForOData);
odataFilterEntries.push(`(contains(properties/displayName,'${pattern}'))`);
}

const pageOfSubscriptions = new Page<Subscription>();
const query = productId ? `?$filter=properties/scope eq '${productId}'` : "";
let query = `?$top=${take}&$skip=${skip}`;

if (odataFilterEntries.length > 0) {
query = Utils.addQueryParameter(query, `$filter=` + odataFilterEntries.join(" and "));
}

try {
const pageContract = await this.mapiClient.get<Page<SubscriptionContract>>(`${userId}/subscriptions${query}`, [await this.mapiClient.getPortalHeader("getSubscriptions")]);
Expand Down
Loading