Skip to content

Commit 849100d

Browse files
committed
feat(portal-next): subscription empty state
1 parent a316a5d commit 849100d

12 files changed

+376
-3
lines changed

gravitee-apim-portal-webui-next/src/app/api-details/api-details.component.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@
5555
i18n="@@apiDocumentation"
5656
>Documentation</a
5757
>
58+
@if (isAuthenticated()) {
59+
<a
60+
mat-tab-link
61+
[routerLink]="['.', 'subscriptions']"
62+
routerLinkActive
63+
#subscriptionsActive="routerLinkActive"
64+
[active]="subscriptionsActive.isActive"
65+
i18n="@@apiSubscriptions"
66+
>My Subscriptions</a
67+
>
68+
}
5869
</nav>
5970
<mat-tab-nav-panel #tabPanel>
6071
<div class="api-details__content">

gravitee-apim-portal-webui-next/src/app/api-details/api-details.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { AsyncPipe } from '@angular/common';
17-
import { Component, Input, OnInit } from '@angular/core';
17+
import { Component, inject, Input, OnInit } from '@angular/core';
1818
import { MatButton } from '@angular/material/button';
1919
import { MatCard, MatCardActions, MatCardContent } from '@angular/material/card';
2020
import { MatIconModule } from '@angular/material/icon';
@@ -27,6 +27,7 @@ import { ApiPictureComponent } from '../../components/api-picture/api-picture.co
2727
import { BannerComponent } from '../../components/banner/banner.component';
2828
import { BreadcrumbNavigationComponent } from '../../components/breadcrumb-navigation/breadcrumb-navigation.component';
2929
import { Api } from '../../entities/api/api';
30+
import { CurrentUserService } from '../../services/current-user.service';
3031

3132
@Component({
3233
selector: 'app-api-details',
@@ -50,6 +51,7 @@ import { Api } from '../../entities/api/api';
5051
})
5152
export class ApiDetailsComponent implements OnInit {
5253
@Input() api!: Api;
54+
isAuthenticated = inject(CurrentUserService).isUserAuthenticated;
5355

5456
constructor(private breadcrumbService: BreadcrumbService) {}
5557

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
3+
Copyright (C) 2024 The Gravitee team (http://gravitee.io)
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
17+
-->
18+
@if (subscriptionsList$ | async; as subscriptions) {
19+
@if (subscriptions.length === 0) {
20+
<div class="api-tab-subscription__empty" id="no-subscriptions">
21+
<header i18n="@@noSubscriptionAvailable" class="api-tab-subscription__empty-header">No subscription found</header>
22+
<p i18n="@@subscribeToApi">Subscribe to our API and your subscription will show up here.</p>
23+
</div>
24+
} @else {
25+
<p>Coming soon</p>
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright (C) 2024 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
.api-tab-subscription__empty {
17+
display: flex;
18+
flex-direction: column;
19+
align-items: center;
20+
justify-content: center;
21+
padding: 20px 0;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (C) 2024 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { HttpTestingController } from '@angular/common/http/testing';
17+
import { ComponentFixture, TestBed } from '@angular/core/testing';
18+
19+
import { ApiTabSubscriptionsComponent } from './api-tab-subscriptions.component';
20+
import { Subscription } from '../../../entities/subscription/subscription';
21+
import { fakeSubscriptionResponse } from '../../../entities/subscription/subscription.fixture';
22+
import { AppTestingModule, TESTING_BASE_URL } from '../../../testing/app-testing.module';
23+
24+
describe('ApiTabSubscriptionsComponent', () => {
25+
let fixture: ComponentFixture<ApiTabSubscriptionsComponent>;
26+
let httpTestingController: HttpTestingController;
27+
28+
beforeEach(async () => {
29+
await TestBed.configureTestingModule({
30+
imports: [ApiTabSubscriptionsComponent, AppTestingModule],
31+
}).compileComponents();
32+
33+
fixture = TestBed.createComponent(ApiTabSubscriptionsComponent);
34+
httpTestingController = TestBed.inject(HttpTestingController);
35+
fixture.componentInstance.apiId = 'testId';
36+
fixture.detectChanges();
37+
});
38+
39+
afterEach(() => {
40+
httpTestingController.verify();
41+
});
42+
43+
describe('empty component', () => {
44+
it('should show empty Subscription list', async () => {
45+
expectSubscriptionList(fakeSubscriptionResponse({ data: [] }), 'testId');
46+
expect(fixture.nativeElement.querySelector('#no-subscriptions')).toBeDefined();
47+
});
48+
});
49+
50+
function expectSubscriptionList(subscriptionResponse: Subscription = fakeSubscriptionResponse(), apiId: string) {
51+
httpTestingController.expectOne(`${TESTING_BASE_URL}/subscriptions?apiId=${apiId}`).flush(subscriptionResponse);
52+
}
53+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (C) 2024 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { AsyncPipe } from '@angular/common';
17+
import { Component, inject, Input, OnInit } from '@angular/core';
18+
import { catchError, map, Observable } from 'rxjs';
19+
import { of } from 'rxjs/internal/observable/of';
20+
21+
import { SubscriptionData } from '../../../entities/subscription/subscription';
22+
import { SubscriptionService } from '../../../services/subscription.service';
23+
24+
@Component({
25+
selector: 'app-api-tab-subscriptions',
26+
standalone: true,
27+
imports: [AsyncPipe],
28+
templateUrl: './api-tab-subscriptions.component.html',
29+
styleUrl: './api-tab-subscriptions.component.scss',
30+
})
31+
export class ApiTabSubscriptionsComponent implements OnInit {
32+
@Input()
33+
apiId!: string;
34+
35+
subscriptionsList$!: Observable<SubscriptionData[]>;
36+
private subscriptionService = inject(SubscriptionService);
37+
38+
constructor() {}
39+
40+
ngOnInit(): void {
41+
this.subscriptionsList$ = this.loadSubscriptions$();
42+
}
43+
44+
private loadSubscriptions$(): Observable<SubscriptionData[]> {
45+
return this.subscriptionService.list(this.apiId).pipe(
46+
map(response => {
47+
return response.data ?? [];
48+
}),
49+
catchError(_ => of([])),
50+
);
51+
}
52+
}

gravitee-apim-portal-webui-next/src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Routes } from '@angular/router';
1818
import { ApiDetailsComponent } from './api-details/api-details.component';
1919
import { ApiTabDetailsComponent } from './api-details/api-tab-details/api-tab-details.component';
2020
import { ApiTabDocumentationComponent } from './api-details/api-tab-documentation/api-tab-documentation.component';
21+
import { ApiTabSubscriptionsComponent } from './api-details/api-tab-subscriptions/api-tab-subscriptions.component';
2122
import { CatalogComponent } from './catalog/catalog.component';
2223
import { LogInComponent } from './log-in/log-in.component';
2324
import { LogOutComponent } from './log-out/log-out.component';
@@ -54,6 +55,12 @@ export const routes: Routes = [
5455
component: ApiTabDocumentationComponent,
5556
data: { breadcrumb: 'Documentation' },
5657
},
58+
{
59+
path: 'subscriptions',
60+
component: ApiTabSubscriptionsComponent,
61+
data: { breadcrumb: 'Subscriptions' },
62+
canActivate: [authGuard],
63+
},
5764
],
5865
},
5966
],
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (C) 2024 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { isFunction } from 'rxjs/internal/util/isFunction';
17+
18+
import { Subscription, SubscriptionData } from './subscription';
19+
20+
export function fakeSubscription(
21+
modifier?: Partial<SubscriptionData> | ((baseSubscription: SubscriptionData) => SubscriptionData),
22+
): SubscriptionData {
23+
const base: SubscriptionData = {
24+
id: '5ac5ca94-160f-4acd-85ca-94160fcacd7d',
25+
application: '99c6cbe6-eead-414d-86cb-e6eeadc14db3',
26+
plan: 'b6f88a31-777f-45aa-b88a-31777fa5aace',
27+
request: '',
28+
reason: 'Subscription has been closed.',
29+
created_at: '2024-04-17T10:33:29Z',
30+
closed_at: '2024-04-17T10:34:05.598Z',
31+
subscribed_by: '4015f9f2-c0a4-4c0c-95f9-f2c0a4fc0c4c',
32+
status: 'REJECTED',
33+
};
34+
35+
if (isFunction(modifier)) {
36+
return modifier(base);
37+
}
38+
39+
return {
40+
...base,
41+
...modifier,
42+
};
43+
}
44+
45+
export function fakeSubscriptionResponse(
46+
modifier?: Partial<Subscription> | ((baseSubscription: Subscription) => Subscription),
47+
): Subscription {
48+
const base: Subscription = {
49+
data: [fakeSubscription()],
50+
links: {
51+
self: 'test',
52+
},
53+
};
54+
55+
if (isFunction(modifier)) {
56+
return modifier(base);
57+
}
58+
59+
return {
60+
...base,
61+
...modifier,
62+
};
63+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2024 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
export interface Subscription {
17+
data: SubscriptionData[];
18+
links: SubscriptionLinks;
19+
}
20+
21+
export interface SubscriptionData {
22+
id?: string;
23+
api?: string;
24+
application?: string;
25+
closed_at?: string;
26+
created_at?: string;
27+
plan?: string;
28+
reason?: string;
29+
request?: string;
30+
status?: string;
31+
subscribed_by?: string;
32+
}
33+
34+
export interface SubscriptionLinks {
35+
self?: string;
36+
}

gravitee-apim-portal-webui-next/src/services/current-user.service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
import { HttpClient } from '@angular/common/http';
17-
import { Injectable, signal, WritableSignal } from '@angular/core';
17+
import { effect, Injectable, signal, WritableSignal } from '@angular/core';
1818
import { isEmpty } from 'lodash';
1919
import { catchError, Observable, tap } from 'rxjs';
2020
import { of } from 'rxjs/internal/observable/of';
@@ -27,11 +27,21 @@ import { User } from '../entities/user/user';
2727
})
2828
export class CurrentUserService {
2929
public user: WritableSignal<User> = signal({});
30+
public isUserAuthenticated: WritableSignal<boolean> = signal(false);
3031

3132
constructor(
3233
private http: HttpClient,
3334
private configuration: ConfigService,
34-
) {}
35+
) {
36+
effect(
37+
() => {
38+
this.isUserAuthenticated.set(!isEmpty(this.user()));
39+
},
40+
{
41+
allowSignalWrites: true,
42+
},
43+
);
44+
}
3545

3646
public isAuthenticated(): boolean {
3747
return !isEmpty(this.user());

0 commit comments

Comments
 (0)