Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions src/app/admin/admin-routing-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAdminModuleRoute } from '../app-routing-paths';

export const REGISTRIES_MODULE_PATH = 'registries';
export const UPDATE_CONFIG_MODULE_PATH = 'update-config';

export function getRegistriesModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
}

export function getUpdateConfigModuleRoute() {
return new URLCombiner(getAdminModuleRoute(), UPDATE_CONFIG_MODULE_PATH).toString();
}
6 changes: 6 additions & 0 deletions src/app/admin/admin-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-a
loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule),
data: {title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert'}
},
{
path: 'update-config',
resolve: { breadcrumb: I18nBreadcrumbResolver },
loadChildren: () => import('./admin-update-config/admin-update-config.module').then((m) => m.AdminUpdateConfigModule),
data: { title: 'admin.update-config.title', breadcrumbKey: 'admin.update-config' }
},
])
],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminUpdateConfigComponent } from './admin-update-config.component';
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
import { SiteAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';

const routes: Routes = [
{
path: '',
component: AdminUpdateConfigComponent,
resolve: { breadcrumb: I18nBreadcrumbResolver },
canActivate: [SiteAdministratorGuard],
data: { title: 'admin.update-config.title', breadcrumbKey: 'admin.update-config' }
}
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminUpdateConfigRoutingModule { }
166 changes: 166 additions & 0 deletions src/app/admin/admin-update-config/admin-update-config.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-cogs me-2"></i>
{{ 'admin.update-config.title' | translate }}
</h2>
<button
class="btn btn-outline-secondary"
(click)="loadConfigFiles()"
[disabled]="loading"
title="{{ 'admin.update-config.refresh.tooltip' | translate }}">
<i class="fas fa-sync-alt" [class.fa-spin]="loading"></i>
{{ 'admin.update-config.refresh' | translate }}
</button>
</div>

<!-- Loading indicator -->
<div *ngIf="loading && configFiles.length === 0" class="text-center py-4">
<div class="spinner-border" role="status">
<span class="visually-hidden">{{ 'admin.update-config.loading' | translate }}</span>
</div>
<p class="mt-2">{{ 'admin.update-config.loading-files' | translate }}</p>
</div>

<!-- File Selection Card -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-folder-open me-2"></i>
{{ 'admin.update-config.file-selection.title' | translate }}
</h5>
<p class="card-text text-muted">
{{ 'admin.update-config.description' | translate }}
</p>

<div class="row align-items-center">
<div class="col-md-6">
<label for="fileSelect" class="form-label fw-bold">
<i class="fas fa-file-code me-2 text-primary"></i>
{{ 'admin.update-config.file-selection.label' | translate }}
</label>
<select
id="fileSelect"
class="form-select form-select-lg"
[(ngModel)]="selectedFile"
(ngModelChange)="onFileSelect($event)"
[disabled]="loading || configFiles.length === 0">
<option [ngValue]="null">{{ getFileSelectionText() }}</option>
<option *ngFor="let file of configFiles" [ngValue]="file">
<i class="fas fa-file me-1"></i>{{ file.fileName || ('admin.update-config.file.no-name' | translate) }}
</option>
</select>
</div>
<div class="col-md-6" *ngIf="selectedFile">
<div class="file-info mt-3 mt-md-0 p-3 bg-light rounded">
<h6 class="mb-2">
<i class="fas fa-info-circle text-info me-1"></i>
{{ 'admin.update-config.file-info.title' | translate }}
</h6>
<small class="text-muted">
<strong>{{ 'admin.update-config.file-info.name' | translate }}:</strong> {{ selectedFile.fileName }}<br>
<strong>{{ 'admin.update-config.file-info.size' | translate }}:</strong> {{ selectedFile.size | number }} {{ 'admin.update-config.file-info.bytes' | translate }}<br>
<strong>{{ 'admin.update-config.file-info.modified' | translate }}:</strong> {{ selectedFile.lastModified | date:'short' }}<br>
<strong>{{ 'admin.update-config.file-info.status' | translate }}:</strong>
<span class="badge bg-success">{{ 'admin.update-config.file-info.ready' | translate }}</span>
</small>
</div>
</div>
</div>
</div>
</div>

<!-- File Content Loading -->
<div *ngIf="loading && selectedFile" class="text-center py-4">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">{{ 'admin.update-config.loading' | translate }}</span>
</div>
<p class="mt-2">{{ 'admin.update-config.loading-content' | translate }}</p>
</div>

<!-- Configuration Editor Card -->
<div *ngIf="selectedFile && !loading" class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title mb-0">
<i class="fas fa-code me-2"></i>
{{ 'admin.update-config.editor.title' | translate }}: {{ selectedFile.fileName }}
</h5>

<!-- Action Buttons -->
<div class="d-flex flex-wrap gap-2" role="group">
<button
class="btn btn-outline-secondary btn-lg"
(click)="resetContent()"
[disabled]="!hasUnsavedChanges() || loading || saving"
[title]="'admin.update-config.buttons.undo.tooltip' | translate">
<i class="fas fa-undo me-2"></i>
{{ 'admin.update-config.buttons.undo' | translate }}
</button>
<button
class="btn btn-outline-warning btn-lg"
(click)="resetToOriginalFile()"
[disabled]="loading || saving"
[title]="'admin.update-config.buttons.reload.tooltip' | translate">
<i class="fas fa-sync me-2"></i>
{{ 'admin.update-config.buttons.reload' | translate }}
</button>
<button
class="btn btn-success btn-lg px-4"
(click)="saveFile()"
[disabled]="!hasUnsavedChanges() || loading || saving"
[title]="'admin.update-config.buttons.save.tooltip' | translate">
<i class="fas fa-save me-2" *ngIf="!saving"></i>
<i class="fas fa-spinner fa-spin me-2" *ngIf="saving"></i>
<strong>{{ saving ? ('admin.update-config.buttons.saving' | translate) : ('admin.update-config.buttons.save' | translate) }}</strong>
</button>
</div>
</div>

<!-- Config File Editor Notice -->
<div class="alert alert-info mb-3 d-flex align-items-center">
<i class="fas fa-cogs me-2 text-primary"></i>
<div>
<strong>{{ 'admin.update-config.editor.notice.title' | translate }}</strong><br>
<small>{{ 'admin.update-config.editor.notice.description' | translate:{ fileName: selectedFile?.fileName } }}</small>
</div>
</div>

<div class="mb-3">
<div *ngIf="hasUnsavedChanges()" class="alert alert-warning py-2">
<i class="fas fa-edit me-1"></i>
{{ 'admin.update-config.validation.unsaved-changes' | translate }}
</div>
</div>

<!-- Text Editor -->
<div class="position-relative">
<label for="xmlContent" class="form-label">
{{ 'admin.update-config.editor.content' | translate }}
</label>
<textarea
id="xmlContent"
class="form-control font-monospace config-editor"
[(ngModel)]="fileContent"
(ngModelChange)="onContentChange()"
placeholder="{{ 'admin.update-config.editor.placeholder' | translate }}"
rows="25"
[disabled]="saving"
spellcheck="false">
</textarea>

<!-- Character count -->
<small class="text-muted position-absolute" style="bottom: 10px; right: 15px; background: rgba(255,255,255,0.8); padding: 2px 6px; border-radius: 3px;">
{{ fileContent.length }} {{ 'admin.update-config.editor.characters' | translate }}
</small>
</div>
</div>
</div>


</div>
</div>
</div>
142 changes: 142 additions & 0 deletions src/app/admin/admin-update-config/admin-update-config.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
.container-fluid {
padding: 1rem;
}

.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
border-radius: 0.5rem;
}

.card-title {
color: #495057;
font-weight: 600;
}

.config-editor {
font-family: 'Courier New', Courier, monospace;
font-size: 0.875rem;
line-height: 1.5;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
resize: vertical;
min-height: 300px;
}

.config-editor:focus {
background-color: #ffffff;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
outline: none;
}

.config-editor.is-invalid {
border-color: #dc3545;
background-color: #fff5f5;
}

.config-editor.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

.form-control.is-invalid {
border-color: #dc3545;
background-image: none;
}

.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

.alert {
border: none;
border-radius: 0.375rem;
}

.alert-info {
background-color: #e7f3ff;
border-left: 4px solid #17a2b8;
color: #0c5460;
}

.alert-danger {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
color: #721c24;
}

.alert-warning {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
color: #856404;
}

.badge {
font-size: 0.75rem;
font-weight: 500;
}

.btn {
border-radius: 0.375rem;
font-weight: 500;
}

.btn:disabled {
opacity: 0.65;
}

.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}

.form-select {
border-radius: 0.375rem;
}

.form-select:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}

/* Loading spinner animation */
.fa-spin {
animation: fa-spin 2s infinite linear;
}

@keyframes fa-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}

/* Responsive adjustments */
@media (max-width: 768px) {
.container-fluid {
padding: 0.5rem;
}

.alert.d-flex {
flex-direction: column;
align-items: flex-start !important;
}

.alert.d-flex > div:last-child {
margin-top: 1rem;
width: 100%;
}

.btn-sm {
width: 100%;
margin-bottom: 0.5rem;
}

.btn-sm:last-child {
margin-bottom: 0;
}
}
Loading
Loading