Skip to content

Commit b36a11d

Browse files
authored
chore(frontend): improve customer features column (#1866)
Signed-off-by: Jakob Steiner <jakob.steiner@glasskube.eu>
1 parent 363f198 commit b36a11d

File tree

8 files changed

+169
-80
lines changed

8 files changed

+169
-80
lines changed

frontend/ui/src/app/artifacts/artifact-licenses/edit-artifact-license.component.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@
6868
: getSelectedItemsCount(artifactCtrl) + ' tags selected'
6969
}}
7070
</span>
71-
<fa-icon [icon]="faChevronDown"></fa-icon>
71+
<fa-icon
72+
class="transition duration-150 ease-in-out"
73+
[class.rotate-x-180]="openedArtifactIdx() === i"
74+
[icon]="faChevronDown" />
7275
</button>
7376

7477
<ng-template

frontend/ui/src/app/components/customer-organizations/customer-organizations.component.html

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -83,34 +83,52 @@
8383
{{ customer.createdAt | date: 'short' }}
8484
</td>
8585
<td class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
86-
<div class="flex flex-wrap gap-1 items-center">
87-
@for (feature of customer.features; track feature) {
88-
<span
89-
class="bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-md dark:bg-gray-700 dark:text-blue-400 border border-blue-400 flex items-center gap-1">
90-
{{ getFeatureLabel(feature) }}
91-
@if (auth.hasAnyRole('read_write', 'admin')) {
92-
<button
93-
type="button"
94-
[attr.aria-label]="'Remove ' + getFeatureLabel(feature) + ' feature'"
95-
title="Disable feature"
96-
(click)="removeFeature(customer, feature)"
97-
class="-mr-1 text-red-400 hover:text-red-600 transition-colors">
98-
<fa-icon [icon]="faXmark"></fa-icon>
99-
</button>
86+
<button
87+
class="flex gap-1 items-center py-2 px-3 text-sm font-medium text-center text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
88+
type="button"
89+
cdkOverlayOrigin
90+
#dropdownTriggerButton
91+
#dropdownTrigger="cdkOverlayOrigin"
92+
(click)="showCustomerFeaturesDropdown(customer, dropdownTriggerButton)">
93+
<span class="flex-grow"> {{ customer.features.length }} features enabled </span>
94+
<fa-icon
95+
class="transition duration-150 ease-in-out"
96+
[class.rotate-x-180]="openCustomerFeaturesDropdownId() === customer.id"
97+
[icon]="faChevronDown" />
98+
</button>
99+
100+
<ng-template
101+
cdkConnectedOverlay
102+
[cdkConnectedOverlayHasBackdrop]="true"
103+
(backdropClick)="hideCustomerFeaturesDropdown()"
104+
[cdkConnectedOverlayBackdropClass]="'transparent'"
105+
[cdkConnectedOverlayOrigin]="dropdownTrigger"
106+
[cdkConnectedOverlayMinWidth]="dropdownWidth + 'px'"
107+
[cdkConnectedOverlayOpen]="openCustomerFeaturesDropdownId() === customer.id">
108+
<div
109+
animate.enter="animate-flip-in-top"
110+
animate.leave="animate-flip-out-top"
111+
style="transform-origin: top center"
112+
class="w-full text-base list-none bg-white divide-y divide-gray-100 rounded-sm shadow-sm dark:bg-gray-700 dark:divide-gray-600">
113+
<ul
114+
class="w-full text-sm font-medium text-gray-900 bg-white border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
115+
@for (f of allCustomerFeatures; track f) {
116+
<li class="w-full border-t border-gray-200 dark:border-gray-600">
117+
<label class="flex items-center ps-3" [class.ms-4]="isFeatureIndent(f)">
118+
<input
119+
type="checkbox"
120+
[checked]="openCustomerFeaturesDropdownCustomer()?.features?.includes(f)"
121+
(change)="toggleFeature(customer, f)"
122+
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-xs focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500" />
123+
<span class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">
124+
{{ getFeatureLabel(f) }}
125+
</span>
126+
</label>
127+
</li>
100128
}
101-
</span>
102-
}
103-
@if (auth.hasAnyRole('read_write', 'admin') && !hasAllFeatures(customer)) {
104-
<button
105-
type="button"
106-
aria-label="Restore all features"
107-
title="Restore all features"
108-
(click)="restoreAllFeatures(customer)"
109-
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
110-
<fa-icon [icon]="faRotate" size="sm"></fa-icon>
111-
</button>
112-
}
113-
</div>
129+
</ul>
130+
</div>
131+
</ng-template>
114132
</td>
115133
<td class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white text-end">
116134
{{ customer.userCount | number }}
@@ -160,7 +178,8 @@
160178

161179
<ng-template #createCustomerDialog>
162180
<div
163-
@modalFlyInOut
181+
animate.enter="modal-fly-in"
182+
animate.leave="modal-fly-out"
164183
style="transform-origin: top center"
165184
class="p-4 w-full mt-12 max-h-full bg-white rounded-lg shadow-sm dark:bg-gray-900">
166185
<!-- Modal header -->

frontend/ui/src/app/components/customer-organizations/customer-organizations.component.ts

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1+
import {OverlayModule} from '@angular/cdk/overlay';
12
import {AsyncPipe, DatePipe, DecimalPipe} from '@angular/common';
2-
import {Component, computed, inject, TemplateRef, viewChild} from '@angular/core';
3+
import {Component, computed, inject, signal, TemplateRef, viewChild} from '@angular/core';
34
import {toSignal} from '@angular/core/rxjs-interop';
45
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
56
import {RouterLink} from '@angular/router';
67
import {CustomerOrganization, CustomerOrganizationFeature, CustomerOrganizationWithUsage} from '@distr-sh/distr-sdk';
78
import {FontAwesomeModule} from '@fortawesome/angular-fontawesome';
89
import {
910
faBuildingUser,
11+
faChevronDown,
1012
faCircleExclamation,
1113
faEdit,
1214
faMagnifyingGlass,
1315
faPlus,
14-
faRotate,
1516
faTrash,
1617
faXmark,
1718
} from '@fortawesome/free-solid-svg-icons';
1819
import {combineLatest, filter, firstValueFrom, map, startWith, Subject, switchMap} from 'rxjs';
1920
import {getFormDisplayedError} from '../../../util/errors';
2021
import {SecureImagePipe} from '../../../util/secureImage';
21-
import {modalFlyInOut} from '../../animations/modal';
2222
import {RequireVendorDirective} from '../../directives/required-role.directive';
2323
import {ArtifactLicensesService} from '../../services/artifact-licenses.service';
2424
import {AuthService} from '../../services/auth.service';
@@ -31,8 +31,6 @@ import {DialogRef, OverlayService} from '../../services/overlay.service';
3131
import {ToastService} from '../../services/toast.service';
3232
import {QuotaLimitComponent} from '../quota-limit.component';
3333

34-
export const ALL_CUSTOMER_FEATURES: CustomerOrganizationFeature[] = ['deployment_targets', 'artifacts', 'alerts'];
35-
3634
@Component({
3735
templateUrl: './customer-organizations.component.html',
3836
imports: [
@@ -45,8 +43,8 @@ export const ALL_CUSTOMER_FEATURES: CustomerOrganizationFeature[] = ['deployment
4543
RouterLink,
4644
RequireVendorDirective,
4745
QuotaLimitComponent,
46+
OverlayModule,
4847
],
49-
animations: [modalFlyInOut],
5048
})
5149
export class CustomerOrganizationsComponent {
5250
protected readonly faMagnifyingGlass = faMagnifyingGlass;
@@ -56,7 +54,7 @@ export class CustomerOrganizationsComponent {
5654
protected readonly faXmark = faXmark;
5755
protected readonly faCircleExclamation = faCircleExclamation;
5856
protected readonly faEdit = faEdit;
59-
protected readonly faRotate = faRotate;
57+
protected readonly faChevronDown = faChevronDown;
6058

6159
private readonly customerOrganizationsService = inject(CustomerOrganizationsService);
6260
private readonly toast = inject(ToastService);
@@ -104,6 +102,19 @@ export class CustomerOrganizationsComponent {
104102
});
105103
protected createFormLoading = false;
106104

105+
protected readonly allCustomerFeatures: readonly CustomerOrganizationFeature[] = [
106+
'deployment_targets',
107+
'alerts',
108+
'artifacts',
109+
];
110+
111+
protected readonly openCustomerFeaturesDropdownId = signal<string | void>(undefined);
112+
protected readonly openCustomerFeaturesDropdownCustomer = computed(() => {
113+
const id = this.openCustomerFeaturesDropdownId();
114+
return id ? this.customerOrganizations()?.find((it) => it.id === id) : undefined;
115+
});
116+
protected dropdownWidth = 0;
117+
107118
protected showCreateDialog() {
108119
this.closeCreateDialog();
109120
this.modalRef = this.overlay.showModal(this.createCustomerDialog());
@@ -198,36 +209,45 @@ export class CustomerOrganizationsComponent {
198209
});
199210
}
200211

201-
protected async removeFeature(customer: CustomerOrganization, feature: CustomerOrganizationFeature): Promise<void> {
202-
const updatedFeatures = customer.features.filter((f) => f !== feature);
203-
try {
204-
await firstValueFrom(
205-
this.customerOrganizationsService.updateCustomerOrganization(customer.id, {
206-
name: customer.name,
207-
imageId: customer.imageId,
208-
features: updatedFeatures,
209-
})
210-
);
211-
this.toast.success(`Feature "${this.getFeatureLabel(feature)}" removed successfully`);
212-
this.refresh$.next();
213-
} catch (e) {
214-
const msg = getFormDisplayedError(e);
215-
if (msg) {
216-
this.toast.error(msg);
217-
}
212+
protected getFeatureLabel(feature: CustomerOrganizationFeature): string {
213+
switch (feature) {
214+
case 'deployment_targets':
215+
return 'Deployments';
216+
case 'artifacts':
217+
return 'Artifacts';
218+
case 'alerts':
219+
return 'Alerts';
220+
default:
221+
return feature;
218222
}
219223
}
220224

221-
protected async restoreAllFeatures(customer: CustomerOrganization): Promise<void> {
225+
protected isFeatureIndent(feature: CustomerOrganizationFeature): boolean {
226+
return feature === 'alerts';
227+
}
228+
229+
protected async toggleFeature(customer: CustomerOrganization, feature: CustomerOrganizationFeature) {
230+
const featureSet = new Set(customer.features);
231+
if (featureSet.has(feature)) {
232+
featureSet.delete(feature);
233+
if (feature === 'deployment_targets') {
234+
featureSet.delete('alerts');
235+
}
236+
} else {
237+
featureSet.add(feature);
238+
if (feature === 'alerts') {
239+
featureSet.add('deployment_targets');
240+
}
241+
}
242+
222243
try {
223244
await firstValueFrom(
224245
this.customerOrganizationsService.updateCustomerOrganization(customer.id, {
225-
name: customer.name,
226-
imageId: customer.imageId,
227-
features: ALL_CUSTOMER_FEATURES,
246+
...customer,
247+
features: Array.from(featureSet),
228248
})
229249
);
230-
this.toast.success('All features restored successfully');
250+
this.toast.success('Customer features updated');
231251
this.refresh$.next();
232252
} catch (e) {
233253
const msg = getFormDisplayedError(e);
@@ -237,20 +257,12 @@ export class CustomerOrganizationsComponent {
237257
}
238258
}
239259

240-
protected hasAllFeatures(customer: CustomerOrganization): boolean {
241-
return customer.features.length === ALL_CUSTOMER_FEATURES.length;
260+
protected showCustomerFeaturesDropdown(customer: CustomerOrganization, btn: HTMLButtonElement) {
261+
this.dropdownWidth = btn.getBoundingClientRect().width;
262+
this.openCustomerFeaturesDropdownId.set(customer.id);
242263
}
243264

244-
protected getFeatureLabel(feature: CustomerOrganizationFeature): string {
245-
switch (feature) {
246-
case 'deployment_targets':
247-
return 'Deployments';
248-
case 'artifacts':
249-
return 'Artifacts';
250-
case 'alerts':
251-
return 'Alerts';
252-
default:
253-
return feature;
254-
}
265+
protected hideCustomerFeaturesDropdown(): void {
266+
this.openCustomerFeaturesDropdownId.set(undefined);
255267
}
256268
}

frontend/ui/src/app/components/nav-bar/nav-bar.component.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ <h1 class="font-display self-center text-xl font-semibold sm:text-2xl whitespace
7777
}
7878
</span>
7979
<fa-icon
80-
class="ml-2 text-gray-900 dark:text-gray-300"
81-
[icon]="organizationsOpened ? faChevronUp : faChevronDown"></fa-icon>
80+
class="ml-2 text-gray-900 dark:text-gray-300 transition duration-150 ease-in-out"
81+
[class.rotate-x-180]="organizationsOpened"
82+
[icon]="faChevronDown" />
8283
</button>
8384
</div>
8485
<ng-template

frontend/ui/src/app/components/side-bar/side-bar.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Agents</span>
5555
<fa-icon
5656
[icon]="faChevronDown"
57-
[class.rotate-180]="agentsSubMenuOpen()"
58-
class="transition duration-150"></fa-icon>
57+
[class.rotate-x-180]="agentsSubMenuOpen()"
58+
class="transition duration-150 ease-in-out"></fa-icon>
5959
</button>
6060
@if (agentsSubMenuOpen()) {
6161
<ul class="space-y-1">
@@ -106,8 +106,8 @@
106106
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Registry</span>
107107
<fa-icon
108108
[icon]="faChevronDown"
109-
[class.rotate-180]="registrySubMenuOpen()"
110-
class="transition duration-150"></fa-icon>
109+
[class.rotate-x-180]="registrySubMenuOpen()"
110+
class="transition duration-150 ease-in-out"></fa-icon>
111111
</button>
112112
@if (registrySubMenuOpen()) {
113113
<ul class="space-y-1">
@@ -146,8 +146,8 @@
146146
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Licenses</span>
147147
<fa-icon
148148
[icon]="faChevronDown"
149-
[class.rotate-180]="licenseSubMenuOpen()"
150-
class="transition duration-150"></fa-icon>
149+
[class.rotate-x-180]="licenseSubMenuOpen()"
150+
class="transition duration-150 ease-in-out"></fa-icon>
151151
</button>
152152
@if (licenseSubMenuOpen()) {
153153
<ul class="space-y-1">

frontend/ui/src/app/licenses/edit-license.component.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
: subjectItemsSelected + ' versions selected'
6060
}}
6161
</span>
62-
<fa-icon [icon]="faChevronDown"></fa-icon>
62+
<fa-icon
63+
class="transition duration-150 ease-in-out"
64+
[class.rotate-x-180]="dropdownOpen()"
65+
[icon]="faChevronDown" />
6366
</button>
6467

6568
<ng-template

frontend/ui/src/styles/animations.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@
2222
}
2323
}
2424

25+
@keyframes flip-top {
26+
from {
27+
transform: rotateX(-90deg);
28+
}
29+
30+
to {
31+
transform: rotateX(0);
32+
}
33+
}
34+
2535
@theme {
2636
--animate-fly-in-right: fly-in-right 150ms ease-out;
2737
--animate-fly-out-right: fly-in-right 150ms ease-in reverse;
38+
39+
--animate-flip-in-top: flip-top 100ms ease-out;
40+
--animate-flip-out-top: flip-top 100ms ease-in reverse;
2841
}
2942

3043
.modal-fly-in {

0 commit comments

Comments
 (0)