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

Removed paging from test console subscriptions dropdown #2664

Merged
merged 1 commit into from
Aug 22, 2024
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 @@ -39,6 +39,7 @@ <h3 class="pt-0">
<div class="form-group">
<select id="authFlow" class="form-control"
data-bind="options: $component.selectedAuthorizationServer()?.grantTypes, value: $component.selectedGrantType, optionsCaption: 'No auth'">
</select>
</div>
</div>
</div>
Expand Down Expand Up @@ -120,11 +121,6 @@ <h3 class="pt-0">
<!-- /ko -->
</div>
<!-- /ko -->
<!-- ko if: $component.nextSubscriptionsPage() || $component.subscriptionsPageNumber() > 1 -->
<pagination
params="{ pageNumber: $component.subscriptionsPageNumber, nextPage: $component.nextSubscriptionsPage }">
</pagination>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ interface SubscriptionOption {
value: string;
}

const maxSubscriptionsPageNumber = 5;

@Component({
selector: "authorization",
template: template,
Expand All @@ -50,8 +48,6 @@ export class Authorization {
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);

Expand Down Expand Up @@ -79,14 +75,13 @@ export class Authorization {
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 @@ -131,11 +126,9 @@ export class Authorization {
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();
}

Expand Down Expand Up @@ -177,6 +170,9 @@ export class Authorization {
const oauthSession = await this.sessionManager.getItem<OAuthSession>(oauthSessionKey);
const recordKey = this.getSessionRecordKey(serverName, scopeOverride);
const storedCredentials = oauthSession?.[recordKey];
if (!storedCredentials) {
return null;
}

try {
/* Trying to check if it's a JWT token and, if yes, whether it got expired. */
Expand Down Expand Up @@ -413,30 +409,26 @@ export class Authorization {
return;
}

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

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

this.nextSubscriptionsPage(!!pageOfSubscriptions.nextLink);

const productsSubscriptions = allSubscriptions.filter(subscription => !this.productService.isProductScope(subscription.scope, this.api().name));
products.forEach(product => {
const keys: SubscriptionOption[] = [];

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

subscriptions.forEach(subscription => {
if (!this.productService.isScopeSuitable(subscription.scope, this.api().name, product.name)) {
productsSubscriptions.forEach(subscription => {
if (!this.productService.isProductScope(subscription.scope, product.name)) {
return;
}

Expand All @@ -456,6 +448,23 @@ export class Authorization {
}
});

const apiSubscriptions = allSubscriptions.filter(subscription => this.productService.isProductScope(subscription.scope, this.api().name));
apiSubscriptions.forEach(subscription => {
const apiKeys: SubscriptionOption[] = [];
apiKeys.push({
name: `Primary: ${subscription.name?.trim() || subscription.primaryKey.substr(0, 4)}`,
value: subscription.primaryKey
});

apiKeys.push({
name: `Secondary: ${subscription.name?.trim() || subscription.secondaryKey.substr(0, 4)}`,
value: subscription.secondaryKey
});
if(apiKeys.length > 0) {
availableProducts.push({ name: "Apis", subscriptionKeys: apiKeys });
}
});

this.isSubscriptionListEmptyDueToFilter(availableProducts.length == 0 && this.subscriptionsPattern() !== undefined);
this.products(availableProducts);
this.subscriptionsLoading(false);
Expand Down
125 changes: 102 additions & 23 deletions src/services/productService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,25 +52,7 @@ export class ProductService {

try {
const pageContract = await this.mapiClient.get<Page<SubscriptionContract>>(`${userId}/subscriptions${query}`, [await this.mapiClient.getPortalHeader("getSubscriptions")]);
const promises: Promise<void>[] = [];
const subscriptions: Subscription[] = [];

for (const subscriptionContract of pageContract.value) {
const subscription = new Subscription(subscriptionContract);

const secretPromise = this.mapiClient
.post<SubscriptionSecrets>(`${userId}/subscriptions/${subscriptionContract.name}/listSecrets`, [await this.mapiClient.getPortalHeader("getSubscriptionSecrets")])
.then(secrets => {
subscription.primaryKey = secrets.primaryKey;
subscription.secondaryKey = secrets.secondaryKey;
});

promises.push(secretPromise);

subscriptions.push(subscription);
}

await Promise.all(promises);
const subscriptions = await this.getSubscriptionsData(pageContract.value, userId);

pageOfSubscriptions.value = subscriptions;
pageOfSubscriptions.count = pageContract.count;
Expand All @@ -87,6 +69,82 @@ export class ProductService {
}
}

/**
* Returns all user products subscriptions for a query.
* @param apiName {string} Api name to check scope in subscriptions.
* @param products {Product[]} Products to check scope in subscriptions.
* @param userId {string} User unique identifier.
* @param searchRequest {SearchQuery} filter.
*/
public async getProductsAllSubscriptions(apiName: string, products: Product[], userId: string, searchRequest?: SearchQuery): Promise<Subscription[]> {
if (!userId) {
throw new Error(`Parameter "userId" not specified.`);
}

const odataFilterEntries = [];

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

let query = "?$top=100&$skip=0";

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

try {
const allContracts = await this.mapiClient.getAll<SubscriptionContract>(`${userId}/subscriptions${query}`, [await this.mapiClient.getPortalHeader("getSubscriptions")]);
return await this.getSubscriptionsData(allContracts, userId, products, apiName);
}
catch (error) {
if (error?.code === "ResourceNotFound") {
return [];
}

throw new Error(`Unable to retrieve subscriptions for user with ID "${userId}". Error: ${error.message}`);
}
}

private async getSubscriptionsData(allContracts: SubscriptionContract[], userId: string, products: Product[] = undefined, apiName: string = undefined): Promise<Subscription[]> {
const promises: Promise<void>[] = [];
const subscriptions: Subscription[] = [];

for (const subscriptionContract of allContracts) {
// stop if we have enough subscriptions
if( subscriptions.length >= Constants.defaultPageSize) {
break;
}

// skip not active subscriptions
if (SubscriptionState[subscriptionContract.properties.state] !== SubscriptionState.active) {
continue;
}

if ((products?.length > 0 || apiName) && !this.isScopeValid(subscriptionContract.properties.scope, apiName, products)) {
continue;
}

const subscription = new Subscription(subscriptionContract);

const secretPromise = this.mapiClient
.post<SubscriptionSecrets>(`${userId}/subscriptions/${subscriptionContract.name}/listSecrets`, [await this.mapiClient.getPortalHeader("getSubscriptionSecrets")])
.then(secrets => {
subscription.primaryKey = secrets.primaryKey;
subscription.secondaryKey = secrets.secondaryKey;
});

promises.push(secretPromise);

subscriptions.push(subscription);
}

await Promise.all(promises);

return subscriptions;
}

/**
* Returns user subscriptions for specified product.
* @param userId {string} User unique identifier.
Expand Down Expand Up @@ -387,18 +445,39 @@ export class ProductService {
}

/**
* Determines if specified subscription scope is suitable in context of an API or a Product.
* Determines if specified subscription scope is suitable in context of a Product.
* @param scope {string} Subscription scope.
* @param apiName {string} ARM name of the API.
* @param productName {string} ARM name of the Product.
*/
public isScopeSuitable(scope: string, apiName: string = null, productName: string = null): boolean {
public isProductScope(scope: string, productName: string): boolean {
if (!scope) {
throw new Error(`Parameter "scope" not specified.`);
}

return scope.endsWith("/apis")
|| (apiName && scope.endsWith(`/apis/${apiName}`))
|| (productName && scope.endsWith(`/products/${productName}`));
}

/**
* Determines if specified subscription scope is suitable in context of an API.
* @param scope {string} Subscription scope.
* @param apiName {string} ARM name of the API.
*/
public isApiScope(scope: string, apiName: string): boolean {
if (!scope) {
throw new Error(`Parameter "scope" not specified.`);
}

return apiName && scope.endsWith(`/apis/${apiName}`);
}

private isScopeValid(scope: string, apiName: string = undefined, products: Product[] = undefined): boolean {
if (!scope) {
throw new Error(`Parameter "scope" not specified.`);
}

return scope.endsWith("/apis")
|| (apiName && scope.endsWith(`/apis/${apiName}`))
|| (products && products.some(product => scope.endsWith(`/products/${product.name}`)));
}
}
2 changes: 2 additions & 0 deletions src/themes/website/styles/navs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

&.show {
display: block;
max-height: 500px;
overflow-x: auto;
}
}

Expand Down
Loading