Skip to content
Draft
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
1 change: 1 addition & 0 deletions AMW_angular/io/src/app/resources/models/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Resource {
name: string;
type: string;
version: string;
release?: string;
defaultRelease: Release;
releases: Release[];
defaultResourceId?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@
</ul>
</div>
}
@if (showMoreMenu()) {
<div ngbDropdown>
<app-button [additionalClasses]="'dropdown-toggle'" [variant]="'secondary'" ngbDropdownToggle>
More
</app-button>
<ul ngbDropdownMenu>
@if (permissions().canTagCurrentState && isApplicationServer()) {
<li ngbDropdownItem>
<a class="dropdown-item" (click)="openTagDialog()" tabindex="0" (keyup.enter)="openTagDialog()">
Tag Current State
</a>
</li>
}
</ul>
</div>
}
</div>

<div class="page-content d-flex gap-3">
Expand Down Expand Up @@ -69,4 +85,5 @@
</div>
}
</div>
<app-tag-current-state [resource]="resource()"></app-tag-current-state>
</app-page>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, computed, inject, signal, Signal } from '@angular/core';
import { Component, computed, inject, signal, Signal, ViewChild } from '@angular/core';
import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component';
import { PageComponent } from '../../layout/page/page.component';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
Expand All @@ -15,6 +15,7 @@ import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from
import { ContextsListComponent } from '../contexts-list/contexts-list.component';
import { ResourcePropertiesComponent } from './resource-properties/resource-properties.component';
import { ResourceReleasesComponent } from './resource-releases/resource-releases.component';
import { TagCurrentStateComponent } from './tag-current-state/tag-current-state.component';

@Component({
selector: 'app-resource-edit',
Expand All @@ -33,6 +34,7 @@ import { ResourceReleasesComponent } from './resource-releases/resource-releases
ResourcePropertiesComponent,
ResourceReleasesComponent,
RouterLink,
TagCurrentStateComponent,
],
templateUrl: './resource-edit.component.html',
styleUrl: './resource-edit.component.scss',
Expand All @@ -43,6 +45,8 @@ export class ResourceEditComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);

@ViewChild(TagCurrentStateComponent) tagCurrentStateComponent!: TagCurrentStateComponent;

id = toSignal(this.route.queryParamMap.pipe(map((params) => Number(params.get('id')))), { initialValue: 0 });
contextId = toSignal(this.route.queryParamMap.pipe(map((params) => Number(params.get('ctx')))), { initialValue: 1 });
resource: Signal<Resource> = this.resourceService.resource;
Expand All @@ -61,12 +65,15 @@ export class ResourceEditComponent {

permissions = computed(() => {
if (this.authService.restrictions().length > 0) {
const resourceTypeName = this.resource()?.type ?? null;
const resourceGroupId = this.resource()?.resourceGroupId ?? null;
return {
canEditResource: this.authService.hasPermission('RESOURCE', 'READ'),
canTestGenerate: this.authService.hasPermission('RESOURCE_TEST_GENERATION', 'READ'),
canTagCurrentState: this.authService.hasPermission('RESOURCE', 'UPDATE', resourceTypeName, resourceGroupId),
};
} else {
return { canEditResource: false, canTestGenerate: false };
return { canEditResource: false, canTestGenerate: false, canTagCurrentState: false };
}
});

Expand All @@ -82,11 +89,23 @@ export class ResourceEditComponent {
() => this.testGenerationAvailable() && this.permissions().canTestGenerate,
);

protected readonly isApplicationServer = computed<boolean>(
() => this.resource()?.type === 'APPLICATIONSERVER',
);

protected readonly showMoreMenu = computed<boolean>(
() => this.permissions().canTagCurrentState && this.isApplicationServer(),
);

loadResourceFromRelease(releaseId: number) {
void this.router.navigate([], {
relativeTo: this.route,
queryParams: { id: releaseId },
queryParamsHandling: 'merge',
});
}

openTagDialog() {
this.tagCurrentStateComponent?.openTagDialog();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<ng-template #tagModal let-modal>
<app-modal-header [title]="'Tag Application Server'" (cancel)="modal.dismiss()"></app-modal-header>
<div class="modal-body">
<p class="mb-3">
You are going to tag the current configuration of the {{ resource().type }} {{ resource().name }} in release
{{ resource().release }}.
</p>
<p class="mb-3">This will save the changes that you have made to the current configuration.</p>

<div class="mb-3">
<label for="tagLabel" class="form-label">Tag name:</label>
<input
type="text"
class="form-control"
id="tagLabel"
[(ngModel)]="tagLabel"
[disabled]="isCreatingTag()"
placeholder="Enter tag name"
/>
</div>

<div class="mb-3">
<label for="tagDate" class="form-label">Tag date (time will be set to 12:00):</label>
<div class="input-group">
<input
class="form-control"
id="tagDate"
placeholder="yyyy-mm-dd"
name="dp"
[(ngModel)]="tagDate"
ngbDatepicker
#d="ngbDatepicker"
[disabled]="isCreatingTag()"
/>
<button
class="btn btn-outline-secondary"
(click)="d.toggle()"
type="button"
[disabled]="isCreatingTag()"
>
<i class="bi bi-calendar3"></i>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<app-button [variant]="'secondary'" (click)="modal.dismiss()" [disabled]="isCreatingTag()">Cancel</app-button>
<app-button [variant]="'primary'" (click)="modal.close()" [disabled]="isCreatingTag() || !tagLabel()">
Create Tag
</app-button>
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Component, inject, input, signal, TemplateRef, ViewChild } from '@angular/core';
import { NgbModal, NgbDatepickerModule, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { FormsModule } from '@angular/forms';
import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component';
import { ButtonComponent } from '../../../shared/button/button.component';
import { ResourceTagsService } from '../../services/resource-tags.service';
import { ToastService } from '../../../shared/elements/toast/toast.service';
import { Resource } from '../../models/resource';

@Component({
selector: 'app-tag-current-state',
standalone: true,
imports: [FormsModule, ModalHeaderComponent, ButtonComponent, NgbDatepickerModule],
templateUrl: './tag-current-state.component.html',
})
export class TagCurrentStateComponent {
private modalService = inject(NgbModal);
private resourceTagsService = inject(ResourceTagsService);
private toastService = inject(ToastService);

@ViewChild('tagModal') tagModal!: TemplateRef<void>;

resource = input.required<Resource>();
tagLabel = signal<string>('');
tagDate = signal<NgbDateStruct>(this.dateToNgbDate(new Date()));
isCreatingTag = signal(false);

openTagDialog() {
const now = new Date();
this.tagLabel.set('');
this.tagDate.set(this.dateToNgbDate(now));
const modalRef = this.modalService.open(this.tagModal);
modalRef.result.then(
() => {
this.createTag();
},
() => {
this.tagLabel.set('');
this.tagDate.set(this.dateToNgbDate(new Date()));
},
);
}

createTag() {
const label = this.tagLabel();
const ngbDate = this.tagDate();

if (!label || label.trim() === '') {
this.toastService.error('Tag label must not be empty.');
return;
}

if (!ngbDate) {
this.toastService.error('Tag date must not be empty.');
return;
}

const date = this.ngbDateToDate(ngbDate);

this.isCreatingTag.set(true);
this.resourceTagsService
.createTag(this.resource().id, {
label: label.trim(),
tagDate: date,
})
.subscribe({
next: () => {
this.toastService.success(`New tag '${label}' created.`);
this.isCreatingTag.set(false);
const now = new Date();
this.tagLabel.set('');
this.tagDate.set(this.dateToNgbDate(now));
},
error: (error) => {
console.error('Failed to create tag:', error);
const errorMessage = error?.error?.message || 'Failed to create tag.';
this.toastService.error(errorMessage);
this.isCreatingTag.set(false);
},
});
}

private dateToNgbDate(date: Date): NgbDateStruct {
return {
year: date.getFullYear(),
month: date.getMonth() + 1,
day: date.getDate(),
};
}

private ngbDateToDate(ngbDate: NgbDateStruct): Date {
return new Date(ngbDate.year, ngbDate.month - 1, ngbDate.day, 12, 0, 0);
}
}
32 changes: 32 additions & 0 deletions AMW_angular/io/src/app/resources/services/resource-tags.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { BaseService } from '../../base/base.service';
import { ResourceTag } from '../models/resource-tag';

export interface TagRequest {
label: string;
tagDate: Date;
}

@Injectable({ providedIn: 'root' })
export class ResourceTagsService extends BaseService {
private http = inject(HttpClient);

getTagsForResource(resourceId: number): Observable<ResourceTag[]> {
return this.http
.get<ResourceTag[]>(`${this.getBaseUrl()}/resources/${resourceId}/tags`, {
headers: this.getHeaders(),
})
.pipe(catchError(this.handleError));
}

createTag(resourceId: number, tagRequest: TagRequest): Observable<ResourceTag> {
return this.http
.post<ResourceTag>(`${this.getBaseUrl()}/resources/${resourceId}/tags`, tagRequest, {
headers: this.getHeaders(),
})
.pipe(catchError(this.handleError));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ private void addRestResourceClasses(Set<Class<?>> resources) {
resources.add(FunctionsRest.class);
resources.add(ServersRest.class);
resources.add(ResourceFunctionsRest.class);
resources.add(ResourceTagsRest.class);

// writers
resources.add(DeploymentDtoCsvBodyWriter.class);
Expand Down
Loading