Skip to content

Commit 330cf98

Browse files
committed
chore(frontend): improve customer features column
Signed-off-by: Jakob Steiner <jakob.steiner@glasskube.eu>
1 parent c69b676 commit 330cf98

File tree

3 files changed

+127
-36
lines changed

3 files changed

+127
-36
lines changed

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

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
</tr>
5656
</thead>
5757
<tbody>
58-
@for (customer of customerOrganizations(); track customer.id) {
58+
@for (customer of customerOrganizations(); track customer.id; let i = $index) {
5959
<tr class="border-t border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700">
6060
<td class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
6161
<app-uuid [uuid]="customer.id" />
@@ -87,34 +87,52 @@
8787
{{ customer.createdAt | date: 'short' }}
8888
</td>
8989
<td class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white">
90-
<div class="flex flex-wrap gap-1 items-center">
91-
@for (feature of customer.features; track feature) {
92-
<span
93-
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">
94-
{{ getFeatureLabel(feature) }}
95-
@if (auth.hasAnyRole('read_write', 'admin')) {
96-
<button
97-
type="button"
98-
[attr.aria-label]="'Remove ' + getFeatureLabel(feature) + ' feature'"
99-
title="Disable feature"
100-
(click)="removeFeature(customer, feature)"
101-
class="-mr-1 text-red-400 hover:text-red-600 transition-colors">
102-
<fa-icon [icon]="faXmark"></fa-icon>
103-
</button>
90+
<button
91+
class="flex gap-1 py-2 px-3 flex items-center 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"
92+
type="button"
93+
cdkOverlayOrigin
94+
#dropdownTriggerButton
95+
#dropdownTrigger="cdkOverlayOrigin"
96+
(click)="showCustomerFeaturesDropdown(customer, dropdownTriggerButton, i)">
97+
<span class="flex-grow"> {{ customer.features.length }} features enabled </span>
98+
<fa-icon
99+
class="transition duration-150 ease-in-out"
100+
[class.rotate-x-180]="openCustomerFeaturesDropdownId() === customer.id"
101+
[icon]="faChevronDown" />
102+
</button>
103+
104+
<ng-template
105+
cdkConnectedOverlay
106+
[cdkConnectedOverlayHasBackdrop]="true"
107+
(backdropClick)="hideCustomerFeaturesDropdown()"
108+
[cdkConnectedOverlayBackdropClass]="'transparent'"
109+
[cdkConnectedOverlayOrigin]="dropdownTrigger"
110+
[cdkConnectedOverlayMinWidth]="dropdownWidth + 'px'"
111+
[cdkConnectedOverlayOpen]="openCustomerFeaturesDropdownId() === customer.id">
112+
<div
113+
animate.enter="animate-flip-in-top"
114+
animate.leave="animate-flip-out-top"
115+
style="transform-origin: top center"
116+
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">
117+
<ul
118+
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">
119+
@for (f of allCustomerFeatures; track f) {
120+
<li class="w-full border-t border-gray-200 dark:border-gray-600">
121+
<label class="flex items-center ps-3" [class.ms-4]="isFeatureIndent(f)">
122+
<input
123+
type="checkbox"
124+
[checked]="openCustomerFeaturesDropdownCustomer()?.features?.includes(f)"
125+
(change)="toggleFeature(customer, f)"
126+
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" />
127+
<span class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">
128+
{{ getFeatureLabel(f) }}
129+
</span>
130+
</label>
131+
</li>
104132
}
105-
</span>
106-
}
107-
@if (auth.hasAnyRole('read_write', 'admin') && !hasAllFeatures(customer)) {
108-
<button
109-
type="button"
110-
aria-label="Restore all features"
111-
title="Restore all features"
112-
(click)="restoreAllFeatures(customer)"
113-
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
114-
<fa-icon [icon]="faRotate" size="sm"></fa-icon>
115-
</button>
116-
}
117-
</div>
133+
</ul>
134+
</div>
135+
</ng-template>
118136
</td>
119137
<td class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap dark:text-white text-end">
120138
{{ customer.userCount | number }}
@@ -164,7 +182,8 @@
164182

165183
<ng-template #createCustomerDialog>
166184
<div
167-
@modalFlyInOut
185+
animate.enter="modal-fly-in"
186+
animate.leave="modal-fly-out"
168187
style="transform-origin: top center"
169188
class="p-4 w-full mt-12 max-h-full bg-white rounded-lg shadow-sm dark:bg-gray-900">
170189
<!-- Modal header -->

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

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
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,
@@ -18,7 +20,6 @@ import {
1820
import {combineLatest, filter, firstValueFrom, map, startWith, Subject, switchMap} from 'rxjs';
1921
import {getFormDisplayedError} from '../../../util/errors';
2022
import {SecureImagePipe} from '../../../util/secureImage';
21-
import {modalFlyInOut} from '../../animations/modal';
2223
import {RequireVendorDirective} from '../../directives/required-role.directive';
2324
import {ArtifactLicensesService} from '../../services/artifact-licenses.service';
2425
import {AuthService} from '../../services/auth.service';
@@ -32,8 +33,6 @@ import {ToastService} from '../../services/toast.service';
3233
import {QuotaLimitComponent} from '../quota-limit.component';
3334
import {UuidComponent} from '../uuid';
3435

35-
export const ALL_CUSTOMER_FEATURES: CustomerOrganizationFeature[] = ['deployment_targets', 'artifacts', 'alerts'];
36-
3736
@Component({
3837
templateUrl: './customer-organizations.component.html',
3938
imports: [
@@ -47,8 +46,8 @@ export const ALL_CUSTOMER_FEATURES: CustomerOrganizationFeature[] = ['deployment
4746
RouterLink,
4847
RequireVendorDirective,
4948
QuotaLimitComponent,
49+
OverlayModule,
5050
],
51-
animations: [modalFlyInOut],
5251
})
5352
export class CustomerOrganizationsComponent {
5453
protected readonly faMagnifyingGlass = faMagnifyingGlass;
@@ -59,6 +58,7 @@ export class CustomerOrganizationsComponent {
5958
protected readonly faCircleExclamation = faCircleExclamation;
6059
protected readonly faEdit = faEdit;
6160
protected readonly faRotate = faRotate;
61+
protected readonly faChevronDown = faChevronDown;
6262

6363
private readonly customerOrganizationsService = inject(CustomerOrganizationsService);
6464
private readonly toast = inject(ToastService);
@@ -106,6 +106,21 @@ export class CustomerOrganizationsComponent {
106106
});
107107
protected createFormLoading = false;
108108

109+
protected readonly allCustomerFeatures: readonly CustomerOrganizationFeature[] = [
110+
'deployment_targets',
111+
'alerts',
112+
'artifacts',
113+
];
114+
115+
protected readonly openCustomerFeaturesDropdownId = signal<string | void>(undefined);
116+
protected readonly openCustomerFeaturesDropdownCustomer = computed(() => {
117+
const id = this.openCustomerFeaturesDropdownId();
118+
const c = id ? this.customerOrganizations()?.find((it) => it.id === id) : undefined;
119+
console.log(c);
120+
return c;
121+
});
122+
protected dropdownWidth = 0;
123+
109124
protected showCreateDialog() {
110125
this.closeCreateDialog();
111126
this.modalRef = this.overlay.showModal(this.createCustomerDialog());
@@ -226,7 +241,7 @@ export class CustomerOrganizationsComponent {
226241
this.customerOrganizationsService.updateCustomerOrganization(customer.id, {
227242
name: customer.name,
228243
imageId: customer.imageId,
229-
features: ALL_CUSTOMER_FEATURES,
244+
features: [...this.allCustomerFeatures],
230245
})
231246
);
232247
this.toast.success('All features restored successfully');
@@ -240,7 +255,7 @@ export class CustomerOrganizationsComponent {
240255
}
241256

242257
protected hasAllFeatures(customer: CustomerOrganization): boolean {
243-
return customer.features.length === ALL_CUSTOMER_FEATURES.length;
258+
return customer.features.length === this.allCustomerFeatures.length;
244259
}
245260

246261
protected getFeatureLabel(feature: CustomerOrganizationFeature): string {
@@ -255,4 +270,48 @@ export class CustomerOrganizationsComponent {
255270
return feature;
256271
}
257272
}
273+
274+
protected isFeatureIndent(feature: CustomerOrganizationFeature): boolean {
275+
return feature === 'alerts';
276+
}
277+
278+
protected async toggleFeature(customer: CustomerOrganization, feature: CustomerOrganizationFeature) {
279+
const featureSet = new Set(customer.features);
280+
if (featureSet.has(feature)) {
281+
featureSet.delete(feature);
282+
if (feature === 'deployment_targets') {
283+
featureSet.delete('alerts');
284+
}
285+
} else {
286+
featureSet.add(feature);
287+
if (feature === 'alerts') {
288+
featureSet.add('deployment_targets');
289+
}
290+
}
291+
292+
try {
293+
await firstValueFrom(
294+
this.customerOrganizationsService.updateCustomerOrganization(customer.id, {
295+
...customer,
296+
features: Array.from(featureSet),
297+
})
298+
);
299+
this.toast.success('Customer features updated');
300+
this.refresh$.next();
301+
} catch (e) {
302+
const msg = getFormDisplayedError(e);
303+
if (msg) {
304+
this.toast.error(msg);
305+
}
306+
}
307+
}
308+
309+
protected showCustomerFeaturesDropdown(customer: CustomerOrganization, btn: HTMLButtonElement, i: number) {
310+
this.dropdownWidth = btn.getBoundingClientRect().width;
311+
this.openCustomerFeaturesDropdownId.set(customer.id);
312+
}
313+
314+
protected hideCustomerFeaturesDropdown(): void {
315+
this.openCustomerFeaturesDropdownId.set(undefined);
316+
}
258317
}

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)