Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a89e7bc96 | ||
|
|
d8ad061ad7 | ||
|
|
c1da21b300 | ||
|
|
ef2dba3b92 | ||
|
|
cf0af07104 | ||
|
|
874d5dfed2 | ||
|
|
91af36866e | ||
|
|
534bc307a7 | ||
|
|
53424f0a7c | ||
|
|
49b5300887 | ||
|
|
c5d730ae22 | ||
|
|
40438fcc1b | ||
|
|
71be18b21f | ||
|
|
1263805d61 | ||
|
|
0e6e4899e5 | ||
|
|
29e50253af | ||
|
|
68b793ea60 | ||
|
|
5f0594c93c | ||
|
|
e7ee226b66 | ||
|
|
ff2f5a1c34 | ||
|
|
fc36794b22 | ||
|
|
75ccca8caf | ||
|
|
c4dfb0283c | ||
|
|
57acdb0300 | ||
|
|
aee28f604f | ||
|
|
8375e53fdf | ||
|
|
db7a9c727d | ||
|
|
6e91af551e | ||
|
|
de5479cc07 | ||
|
|
807058e40d | ||
|
|
c6022b0e22 | ||
|
|
9aed6e0d43 | ||
|
|
64b664e625 | ||
|
|
49f1a2fbf2 | ||
|
|
6bfe890cd9 | ||
|
|
3e70f85644 | ||
|
|
482805b5cf | ||
|
|
ffda17e6b1 | ||
|
|
7396843bc6 | ||
|
|
4f75ecb3e0 |
@@ -1,8 +1,9 @@
|
|||||||
export class ReportBuilder {
|
export class ReportBuilder {
|
||||||
public report_id: number;
|
public report_id: number;
|
||||||
public report_name:string;
|
public report_name: string;
|
||||||
public description: string;
|
public description: string;
|
||||||
public report_tags: string;
|
public report_tags: string;
|
||||||
public servicename:string;
|
public servicename: string;
|
||||||
|
// Add SureConnect reference
|
||||||
|
public sureConnectId: number | null;
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,22 @@ export interface DashboardContentModel {
|
|||||||
component?: any;
|
component?: any;
|
||||||
name: string;
|
name: string;
|
||||||
type?:string;
|
type?:string;
|
||||||
// Common properties
|
// Chart properties
|
||||||
|
xAxis?: string;
|
||||||
|
yAxis?: string | string[];
|
||||||
|
chartType?: string;
|
||||||
|
charttitle?: string;
|
||||||
|
chartlegend?: boolean;
|
||||||
|
showlabel?: boolean;
|
||||||
|
chartcolor?: boolean;
|
||||||
|
slices?: boolean;
|
||||||
|
donut?: boolean;
|
||||||
|
charturl?: string;
|
||||||
|
chartparameter?: string;
|
||||||
|
datastore?: string;
|
||||||
table?: string;
|
table?: string;
|
||||||
|
datasource?: string;
|
||||||
|
fieldName?: string;
|
||||||
connection?: string;
|
connection?: string;
|
||||||
baseFilters?: any[];
|
baseFilters?: any[];
|
||||||
// Common filter properties
|
// Common filter properties
|
||||||
@@ -37,6 +51,11 @@ export interface DashboardContentModel {
|
|||||||
drilldownParameter?: string;
|
drilldownParameter?: string;
|
||||||
drilldownFilters?: any[];
|
drilldownFilters?: any[];
|
||||||
drilldownLayers?: any[];
|
drilldownLayers?: any[];
|
||||||
|
// Compact filter properties
|
||||||
|
filterKey?: string;
|
||||||
|
filterType?: string;
|
||||||
|
filterLabel?: string;
|
||||||
|
filterOptions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardModel {
|
export interface DashboardModel {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<span
|
<span
|
||||||
style="margin-right: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"
|
style="margin-right: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"
|
||||||
title="{{user.url_endpoint}}" (click)="showFullUrl(user.url_endpoint)">
|
title="{{user.url_endpoint}}" (click)="showFullUrl(user.url_endpoint)">
|
||||||
{{user.url | slice:0:30}}{{user.url_endpoint.length > 30 ? '...' : ''}}
|
{{user.url | slice:0:30}}{{user?.url_endpoint?.length > 30 ? '...' : ''}}
|
||||||
</span>
|
</span>
|
||||||
<button class="btn btn-icon btn-sm" (click)="copyToClipboard(user.url_endpoint)" title="Copy URL">
|
<button class="btn btn-icon btn-sm" (click)="copyToClipboard(user.url_endpoint)" title="Copy URL">
|
||||||
<clr-icon shape="copy-to-clipboard"></clr-icon>
|
<clr-icon shape="copy-to-clipboard"></clr-icon>
|
||||||
@@ -142,35 +142,56 @@
|
|||||||
|
|
||||||
<!-- who column -->
|
<!-- who column -->
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
<div style="display: flex; align-items: center; gap: 1px; flex-wrap: nowrap;">
|
<clr-signpost>
|
||||||
<clr-signpost>
|
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||||
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
<h5 style="margin-top: 0">Who Column</h5>
|
||||||
<h5 style="margin-top: 0">Who Column</h5>
|
<div>Account ID: <code class="clr-code">{{user.accountId}}</code></div>
|
||||||
<div>Account ID: <code class="clr-code">{{user.accountId}}</code></div>
|
<div>Created At: <code class="clr-code">{{user.createdAt| date}}</code></div>
|
||||||
<div>Created At: <code class="clr-code">{{user.createdAt| date}}</code></div>
|
<div>Created By: <code class="clr-code">{{user.createdBy}}</code></div>
|
||||||
<div>Created By: <code class="clr-code">{{user.createdBy}}</code></div>
|
<div>Updated At: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
||||||
<div>Updated At: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
<div>Updated By: <code class="clr-code">{{user.updatedBy}}</code></div>
|
||||||
<div>Updated By: <code class="clr-code">{{user.updatedBy}}</code></div>
|
</clr-signpost-content>
|
||||||
</clr-signpost-content>
|
</clr-signpost>
|
||||||
</clr-signpost>
|
|
||||||
|
|
||||||
<!-- New JSON Update button -->
|
<!-- New JSON Update button -->
|
||||||
<button class="btn btn-icon btn-sm" (click)="updateJson(user.id)" title="Update JSON">
|
<button class="btn btn-icon" (click)="updateJson(user.id)" title="Update JSON">
|
||||||
<clr-icon shape="refresh"></clr-icon>
|
<clr-icon shape="refresh"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Calculated Field button -->
|
<!-- Calculated Field button -->
|
||||||
<button class="btn btn-icon btn-sm" (click)="fetchAvailableKeys(user, false)" title="Create Calculated Field">
|
<button class="btn btn-icon" (click)="fetchAvailableKeys(user)" title="Create Calculated Field">
|
||||||
<clr-icon shape="calculator"></clr-icon>
|
<clr-icon shape="calculator"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Group By button -->
|
<!-- Group By button -->
|
||||||
<button class="btn btn-icon btn-sm" (click)="openGroupByModal(user)" title="Group By Configuration">
|
<button class="btn btn-icon" (click)="openGroupByModal(user)" title="Group By Configuration">
|
||||||
<clr-icon shape="group"></clr-icon>
|
<clr-icon shape="group"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
<!-- Blending Keys button (only shown for blending type with non-empty blending_lakeids) -->
|
||||||
|
<button class="btn btn-icon"
|
||||||
|
(click)="fetchBlendingKeys(user)"
|
||||||
|
title="Configure Blending Query"
|
||||||
|
*ngIf="canShowBlendingAction(user)">
|
||||||
|
<clr-icon shape="code"></clr-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Field Mapping button -->
|
||||||
|
<button class="btn btn-icon"
|
||||||
|
(click)="openFieldMappingModal(user)"
|
||||||
|
title="Field Mapping"
|
||||||
|
*ngIf="user.url && user.sure_connect_id">
|
||||||
|
<clr-icon shape="map"></clr-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Scheduler button -->
|
||||||
|
<button class="btn btn-icon"
|
||||||
|
(click)="openSchedulerModal(user)"
|
||||||
|
title="Scheduler">
|
||||||
|
<clr-icon shape="clock"></clr-icon>
|
||||||
|
</button>
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
|
|
||||||
<!-- who colmn -->
|
<!-- who colmn -->
|
||||||
@@ -434,8 +455,32 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add ref_datalake_id dropdown -->
|
<!-- Data Lake Type -->
|
||||||
<div class="clr-col-sm-12">
|
<div class="clr-col-sm-12">
|
||||||
|
<label>Data Lake Type</label>
|
||||||
|
<select class="clr-input" [(ngModel)]="rowSelected.datalake_type" name="datalake_type">
|
||||||
|
<option *ngFor="let type of dataLakeTypes" [value]="type.value">{{ type.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blending Data Lakes (only show when blending type is selected) -->
|
||||||
|
<div class="clr-col-sm-12" *ngIf="rowSelected.datalake_type === 'blending'">
|
||||||
|
<label>Blending Data Lakes</label>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<div *ngFor="let dataLake of dataLakeList" class="checkbox-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[value]="dataLake.id"
|
||||||
|
(change)="onEditBlendingLakeCheckboxChange($event, dataLake.id)"
|
||||||
|
[checked]="isEditBlendingLakeSelected(dataLake.id)">
|
||||||
|
<label>{{ dataLake.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="clr-subtext">Select data lakes to blend</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reference Data Lake (only show when normal type is selected) -->
|
||||||
|
<div class="clr-col-sm-12" *ngIf="rowSelected.datalake_type === 'normal'">
|
||||||
<label>Reference Data Lake</label>
|
<label>Reference Data Lake</label>
|
||||||
<select class="clr-input" [(ngModel)]="rowSelected.ref_datalake_id" name="ref_datalake_id">
|
<select class="clr-input" [(ngModel)]="rowSelected.ref_datalake_id" name="ref_datalake_id">
|
||||||
<option value="">Select Data Lake</option>
|
<option value="">Select Data Lake</option>
|
||||||
@@ -563,8 +608,32 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add ref_datalake_id dropdown -->
|
<!-- Data Lake Type -->
|
||||||
<div class="clr-col-sm-12">
|
<div class="clr-col-sm-12">
|
||||||
|
<label>Data Lake Type</label>
|
||||||
|
<select formControlName="datalake_type">
|
||||||
|
<option *ngFor="let type of dataLakeTypes" [value]="type.value">{{ type.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blending Data Lakes (only show when blending type is selected) -->
|
||||||
|
<div class="clr-col-sm-12" *ngIf="entryForm.get('datalake_type')?.value === 'blending'">
|
||||||
|
<label>Blending Data Lakes</label>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<div *ngFor="let dataLake of dataLakeList" class="checkbox-item">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[value]="dataLake.id"
|
||||||
|
(change)="onBlendingLakeCheckboxChange($event, dataLake.id)"
|
||||||
|
[checked]="isBlendingLakeSelected(dataLake.id)">
|
||||||
|
<label>{{ dataLake.name }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="clr-subtext">Select data lakes to blend</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reference Data Lake (only show when normal type is selected) -->
|
||||||
|
<div class="clr-col-sm-12" *ngIf="entryForm.get('datalake_type')?.value === 'normal'">
|
||||||
<label>Reference Data Lake</label>
|
<label>Reference Data Lake</label>
|
||||||
<select formControlName="ref_datalake_id">
|
<select formControlName="ref_datalake_id">
|
||||||
<option value="">Select Data Lake</option>
|
<option value="">Select Data Lake</option>
|
||||||
@@ -916,4 +985,199 @@
|
|||||||
</div>
|
</div>
|
||||||
</clr-modal>
|
</clr-modal>
|
||||||
|
|
||||||
<!-- htmlpopup -->
|
<!-- Blending Keys Panel -->
|
||||||
|
<div class="blending-keys-panel" *ngIf="showBlendingKeys">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>Blending Configuration</h3>
|
||||||
|
<button class="btn btn-icon" (click)="closeBlendingKeys()" title="Close">
|
||||||
|
<clr-icon shape="close"></clr-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-content">
|
||||||
|
<!-- Display keys for each blending lake -->
|
||||||
|
<div class="keys-container" *ngFor="let keyData of blendingKeysData">
|
||||||
|
<h4>Data Lake {{ keyData.lakeId }} - Table: {{ keyData.tableName }}</h4>
|
||||||
|
<div class="headers-container">
|
||||||
|
<span class="header-tag" *ngFor="let header of keyData.headers">{{ header }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SQL Query Editor -->
|
||||||
|
<div class="sql-editor-container">
|
||||||
|
<h4>SQL Query Builder</h4>
|
||||||
|
<textarea class="clr-textarea sql-textarea"
|
||||||
|
[(ngModel)]="sqlQueryText"
|
||||||
|
placeholder="Enter your SQL query here using the available headers..."
|
||||||
|
rows="6"></textarea>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateSqlQuery()">Update SQL Query</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blending Configuration Modal -->
|
||||||
|
<clr-modal [(clrModalOpen)]="showBlendingKeys" [clrModalSize]="'xl'" [clrModalStaticBackdrop]="true">
|
||||||
|
<h3 class="modal-title">Blending Configuration</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="blending-config-container">
|
||||||
|
<!-- Display keys for each blending lake -->
|
||||||
|
<div class="keys-section" *ngIf="blendingKeysData && blendingKeysData.length > 0">
|
||||||
|
<h4 class="section-title">
|
||||||
|
<clr-icon shape="data-field" class="section-icon"></clr-icon>
|
||||||
|
Available Data Sources
|
||||||
|
</h4>
|
||||||
|
<div class="keys-container">
|
||||||
|
<div class="key-card" *ngFor="let keyData of blendingKeysData">
|
||||||
|
<div class="key-header">
|
||||||
|
<h5>Data Lake {{ keyData.lakeId }} - Table: {{ keyData.tableName }}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="headers-container">
|
||||||
|
<span class="header-tag" *ngFor="let header of keyData.headers"
|
||||||
|
(click)="insertHeaderIntoQuery(header)">
|
||||||
|
{{ header }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SQL Query Editor -->
|
||||||
|
<div class="sql-editor-section">
|
||||||
|
<h4 class="section-title">
|
||||||
|
<clr-icon shape="code" class="section-icon"></clr-icon>
|
||||||
|
SQL Query Builder
|
||||||
|
</h4>
|
||||||
|
<div class="sql-editor-container">
|
||||||
|
<textarea class="clr-textarea sql-textarea"
|
||||||
|
[(ngModel)]="sqlQueryText"
|
||||||
|
placeholder="Enter your SQL query here using the available headers. Click on header names above to insert them into your query."
|
||||||
|
rows="8"></textarea>
|
||||||
|
<div class="editor-hint">
|
||||||
|
<small class="clr-subtext">Tip: Click on header names above to automatically insert them into your query</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" (click)="closeBlendingKeys()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="updateSqlQuery()">Update SQL Query</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
|
|
||||||
|
<!-- Field Mapping Modal -->
|
||||||
|
<clr-modal [(clrModalOpen)]="showFieldMappingModal" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||||
|
<h3 class="modal-title">Field Mapping Configuration</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="field-mapping-container">
|
||||||
|
<div class="instructions">
|
||||||
|
<p>Map original field names to new names. Leave mapping blank to exclude field from mapping.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mapping-table-container" *ngIf="fieldMappingData && fieldMappingData.length > 0">
|
||||||
|
<table class="table mapping-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50%">Original Field Name</th>
|
||||||
|
<th width="50%">Mapped Field Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let field of fieldMappingData; let i = index">
|
||||||
|
<td class="original-field">
|
||||||
|
<span class="field-tag">{{ field.original }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text"
|
||||||
|
class="clr-input mapping-input"
|
||||||
|
[(ngModel)]="field.mapped"
|
||||||
|
placeholder="Enter new field name"
|
||||||
|
(ngModelChange)="updateFieldMapping(i, $event)">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="no-data-message" *ngIf="fieldMappingData && fieldMappingData.length === 0">
|
||||||
|
<p>No fields available for mapping. Please check the data source configuration.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" (click)="closeFieldMappingModal()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" (click)="updateFieldMappings()" [disabled]="!fieldMappingData || fieldMappingData.length === 0">Update Field Mappings</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
|
|
||||||
|
<!-- Scheduler Modal -->
|
||||||
|
<clr-modal [(clrModalOpen)]="showSchedulerModal" [clrModalSize]="'md'" [clrModalStaticBackdrop]="true">
|
||||||
|
<h3 class="modal-title">Job Scheduler</h3>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="scheduler-container">
|
||||||
|
<div class="data-lake-info">
|
||||||
|
<h4>Data Lake: {{ selectedSchedulerItem?.name }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Information -->
|
||||||
|
<div class="job-info-section" *ngIf="schedulerJob">
|
||||||
|
<div class="job-header">
|
||||||
|
<h5>Job Information</h5>
|
||||||
|
<span class="badge" [ngClass]="getStatusBadgeClass(schedulerJob.status)">
|
||||||
|
{{ schedulerJob.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="job-details">
|
||||||
|
<div class="detail-row">
|
||||||
|
<label>Job Name:</label>
|
||||||
|
<span>{{ schedulerJob.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<label>Job Type:</label>
|
||||||
|
<span>{{ schedulerJob.jobType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<label>Description:</label>
|
||||||
|
<span>{{ schedulerJob.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Actions -->
|
||||||
|
<div class="job-actions">
|
||||||
|
<button class="btn btn-warning" (click)="pauseJob()" *ngIf="schedulerJob.status === 'RUNNING'">
|
||||||
|
<clr-icon shape="pause"></clr-icon> Pause Job
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success" (click)="resumeJob()" *ngIf="schedulerJob.status === 'PAUSED'">
|
||||||
|
<clr-icon shape="play"></clr-icon> Resume Job
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" (click)="stopJob()" *ngIf="schedulerJob.status !== 'STOPPED'">
|
||||||
|
<clr-icon shape="stop"></clr-icon> Stop Job
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" (click)="restartJob()" *ngIf="schedulerJob.status === 'STOPPED'">
|
||||||
|
<clr-icon shape="play"></clr-icon> Restart Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Job Found -->
|
||||||
|
<div class="no-job-section" *ngIf="schedulerJob === null">
|
||||||
|
<div class="no-job-message">
|
||||||
|
<p>No scheduled job found for this Data Lake.</p>
|
||||||
|
<p>Would you like to create a new job?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-job-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createJob()">
|
||||||
|
<clr-icon shape="plus"></clr-icon> Create Job
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline" (click)="closeSchedulerModal()">Close</button>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
@@ -384,3 +384,547 @@
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Data Lake Type Styles */
|
||||||
|
.clr-subtext {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-multiple] {
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox Styles for Blending Data Lakes */
|
||||||
|
.checkbox-container {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-item label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blending Keys Panel Styles */
|
||||||
|
.blending-keys-panel {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-top: 1px solid #ccc;
|
||||||
|
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys-container h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tag {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-editor-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-textarea {
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blending Configuration Modal Styles */
|
||||||
|
.blending-config-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: #0072a0;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys-section {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-card {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-header h5 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tag:hover {
|
||||||
|
background-color: #bbdefb;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-editor-section {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-editor-container {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-textarea {
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-hint {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field Mapping Modal Styles */
|
||||||
|
.field-mapping-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions p {
|
||||||
|
margin: 0;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-table th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 12px 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.original-field {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-tag {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border: 1px solid #bdbdbd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapping-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0072a0;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 114, 160, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scheduler Modal Styles */
|
||||||
|
.scheduler-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-lake-info h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-info-section {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-light {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-details {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row label {
|
||||||
|
font-weight: 500;
|
||||||
|
width: 120px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-actions .btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-message p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-message p:first-child {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-job-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-job-actions .btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
/* Scheduler Modal Styles */
|
||||||
|
.scheduler-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-lake-info h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-info-section {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-header h5 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background-color: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-light {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-details {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row label {
|
||||||
|
font-weight: 500;
|
||||||
|
width: 120px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-actions .btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-message p {
|
||||||
|
margin: 10px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-job-message p:first-child {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-job-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-job-actions .btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { AlertService } from 'src/app/services/alert.service';
|
import { AlertService } from 'src/app/services/alert.service';
|
||||||
import { Data_lakeservice } from './Data_lake.service';
|
import { Data_lakeservice } from './Data_lake.service';
|
||||||
|
import { SchedulerService } from 'src/app/services/scheduler.service';
|
||||||
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators, ValidationErrors } from '@angular/forms';
|
import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators, ValidationErrors } from '@angular/forms';
|
||||||
import { ExtensionService } from 'src/app/services/fnd/extension.service';
|
import { ExtensionService } from 'src/app/services/fnd/extension.service';
|
||||||
import { DashboardContentModel2 } from 'src/app/models/builder/dashboard';
|
import { DashboardContentModel2 } from 'src/app/models/builder/dashboard';
|
||||||
@@ -10,6 +11,7 @@ import { UserInfoService } from 'src/app/services/user-info.service';
|
|||||||
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
import { SureconnectService } from '../sureconnect/sureconnect.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { DataLakeSchedulerService } from './dataLakescheduler.service';
|
||||||
declare var JsBarcode: any;
|
declare var JsBarcode: any;
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-Data_lake',
|
selector: 'app-Data_lake',
|
||||||
@@ -66,6 +68,12 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
dataLakeList: any[] = [];
|
dataLakeList: any[] = [];
|
||||||
selectedDataLake: any = null;
|
selectedDataLake: any = null;
|
||||||
|
|
||||||
|
// New properties for data lake type
|
||||||
|
dataLakeTypes = [
|
||||||
|
{ value: 'normal', label: 'Normal' },
|
||||||
|
{ value: 'blending', label: 'Blending' }
|
||||||
|
];
|
||||||
|
|
||||||
// Calculated field properties
|
// Calculated field properties
|
||||||
calculatedFieldModalOpen = false;
|
calculatedFieldModalOpen = false;
|
||||||
availableKeys: string[] = [];
|
availableKeys: string[] = [];
|
||||||
@@ -99,10 +107,32 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
|
|
||||||
selectedAggregationFields: { field: string; operation: string }[] = [];
|
selectedAggregationFields: { field: string; operation: string }[] = [];
|
||||||
|
|
||||||
|
// New property to store selected blending lake IDs as string
|
||||||
|
selectedBlendingLakeIds: string[] = [];
|
||||||
|
editSelectedBlendingLakeIds: string[] = [];
|
||||||
|
|
||||||
|
// New properties for blending functionality
|
||||||
|
showBlendingKeys = false;
|
||||||
|
blendingKeysData: any[] = [];
|
||||||
|
sqlQueryText = '';
|
||||||
|
selectedBlendingItem: any = null;
|
||||||
|
|
||||||
|
// New properties for field mapping
|
||||||
|
showFieldMappingModal = false;
|
||||||
|
fieldMappingData: any[] = [];
|
||||||
|
fieldMappings: { [key: string]: string } = {};
|
||||||
|
selectedMappingItem: any = null;
|
||||||
|
|
||||||
|
// New properties for scheduler functionality
|
||||||
|
showSchedulerModal = false;
|
||||||
|
schedulerJob: any = null;
|
||||||
|
selectedSchedulerItem: any = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private extensionService: ExtensionService,
|
private extensionService: ExtensionService,
|
||||||
private userInfoService: UserInfoService,
|
private userInfoService: UserInfoService,
|
||||||
private mainService: Data_lakeservice,
|
private mainService: Data_lakeservice,
|
||||||
|
private schedulerService: DataLakeSchedulerService,
|
||||||
private alertService: AlertService,
|
private alertService: AlertService,
|
||||||
private toastr: ToastrService,
|
private toastr: ToastrService,
|
||||||
private _fb: FormBuilder,
|
private _fb: FormBuilder,
|
||||||
@@ -124,23 +154,18 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
this.getDataLakeList(); // Fetch Data Lake list for reference dropdown
|
this.getDataLakeList(); // Fetch Data Lake list for reference dropdown
|
||||||
this.entryForm = this._fb.group({
|
this.entryForm = this._fb.group({
|
||||||
name: [null],
|
name: [null],
|
||||||
|
|
||||||
url: [null],
|
url: [null],
|
||||||
|
|
||||||
schedule: [null],
|
schedule: [null],
|
||||||
|
|
||||||
cron_job: [null],
|
cron_job: [null],
|
||||||
|
|
||||||
json: [null],
|
json: [null],
|
||||||
|
|
||||||
url_endpoint: [null],
|
url_endpoint: [null],
|
||||||
|
|
||||||
batch_volume: [null],
|
batch_volume: [null],
|
||||||
|
sure_connect_id: [null],
|
||||||
|
ref_datalake_id: [null],
|
||||||
|
datalake_type: ['normal'], // Default to normal
|
||||||
|
blending_lakeids: [[]] // Array for multiple selections
|
||||||
|
});
|
||||||
|
|
||||||
sure_connect_id: [null], // Add SureConnect field
|
|
||||||
ref_datalake_id: [null], // Add Data Lake reference field
|
|
||||||
|
|
||||||
}); // component_button200
|
|
||||||
// form code start
|
// form code start
|
||||||
this.extensionService.getJsonObjectsByFormCodeList(this.formcode).subscribe(data => {
|
this.extensionService.getJsonObjectsByFormCodeList(this.formcode).subscribe(data => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
@@ -240,8 +265,7 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
// Fetch Data Lake list for reference dropdown
|
// Fetch Data Lake list for reference dropdown
|
||||||
getDataLakeList() {
|
getDataLakeList() {
|
||||||
this.mainService.getAll().subscribe((data: any[]) => {
|
this.mainService.getAll().subscribe((data: any[]) => {
|
||||||
// Filter out the current item to avoid self-reference
|
this.dataLakeList = data;
|
||||||
this.dataLakeList = data.filter(item => item.id !== (this.rowSelected?.id || 0));
|
|
||||||
console.log('Data Lake List:', this.dataLakeList);
|
console.log('Data Lake List:', this.dataLakeList);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.log('Error fetching Data Lake list:', error);
|
console.log('Error fetching Data Lake list:', error);
|
||||||
@@ -686,8 +710,16 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
this.editCronExpression = row.cron_job || '';
|
this.editCronExpression = row.cron_job || '';
|
||||||
// Set the selected SureConnect for edit form
|
// Set the selected SureConnect for edit form
|
||||||
this.selectedSureConnect = row.sure_connect_id || null;
|
this.selectedSureConnect = row.sure_connect_id || null;
|
||||||
// Set the selected Data Lake for edit form
|
|
||||||
this.selectedDataLake = row.ref_datalake_id || null;
|
// Initialize blending lake IDs for edit form from string
|
||||||
|
if (row.blending_lakeids && typeof row.blending_lakeids === 'string') {
|
||||||
|
this.editSelectedBlendingLakeIds = row.blending_lakeids.split(',').filter(id => id.trim() !== '');
|
||||||
|
} else if (row.blending_lakeids && Array.isArray(row.blending_lakeids)) {
|
||||||
|
this.editSelectedBlendingLakeIds = row.blending_lakeids.map(id => id.toString());
|
||||||
|
} else {
|
||||||
|
this.editSelectedBlendingLakeIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Use setTimeout to ensure the component has time to initialize
|
// Use setTimeout to ensure the component has time to initialize
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.modalEdit = true;
|
this.modalEdit = true;
|
||||||
@@ -838,18 +870,22 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
goToAdd(row) {
|
goToAdd(row) {
|
||||||
this.modalAdd = true;
|
this.modalAdd = false;
|
||||||
this.addCronExpression = '';
|
this.addCronExpression = '';
|
||||||
this.selectedSureConnect = null; // Reset SureConnect selection
|
this.selectedSureConnect = null; // Reset SureConnect selection
|
||||||
this.selectedDataLake = null; // Reset Data Lake selection
|
|
||||||
this.submitted = false;
|
this.submitted = false;
|
||||||
|
// Reset blending lake IDs for add form
|
||||||
|
this.selectedBlendingLakeIds = [];
|
||||||
|
|
||||||
// Reset the form control for cron_job and sure_connect_id
|
// Reset the form control for cron_job and sure_connect_id
|
||||||
if (this.entryForm) {
|
if (this.entryForm) {
|
||||||
if (this.entryForm.get('cron_job')) {
|
if (this.entryForm.get('cron_job')) {
|
||||||
this.entryForm.get('cron_job')?.setValue('');
|
this.entryForm.get('cron_job')?.setValue('');
|
||||||
}
|
}
|
||||||
|
// Reset blending_lakeids to empty string
|
||||||
|
this.entryForm.get('blending_lakeids')?.setValue('');
|
||||||
}
|
}
|
||||||
|
this.modalAdd = true;
|
||||||
}
|
}
|
||||||
submitted = false;
|
submitted = false;
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
@@ -1290,4 +1326,402 @@ export class Data_lakeComponent implements OnInit {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to check if blending action should be available
|
||||||
|
canShowBlendingAction(item: any): boolean {
|
||||||
|
return item.datalake_type === 'blending' &&
|
||||||
|
item.blending_lakeids &&
|
||||||
|
item.blending_lakeids.trim() !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to insert header into SQL query at cursor position
|
||||||
|
insertHeaderIntoQuery(header: string) {
|
||||||
|
const textarea = document.querySelector('.sql-textarea') as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
const start = textarea.selectionStart;
|
||||||
|
const end = textarea.selectionEnd;
|
||||||
|
const text = textarea.value;
|
||||||
|
const before = text.substring(0, start);
|
||||||
|
const after = text.substring(end, text.length);
|
||||||
|
|
||||||
|
// Insert the header at cursor position
|
||||||
|
this.sqlQueryText = before + header + after;
|
||||||
|
|
||||||
|
// Set cursor position after the inserted text
|
||||||
|
setTimeout(() => {
|
||||||
|
textarea.selectionStart = start + header.length;
|
||||||
|
textarea.selectionEnd = start + header.length;
|
||||||
|
textarea.focus();
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
// Fallback: append to end of text
|
||||||
|
this.sqlQueryText += header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to fetch blending keys
|
||||||
|
fetchBlendingKeys(item: any) {
|
||||||
|
this.selectedBlendingItem = item;
|
||||||
|
this.blendingKeysData = [];
|
||||||
|
this.showBlendingKeys = true;
|
||||||
|
|
||||||
|
// Initialize SQL query text from existing data
|
||||||
|
if (item.sqlquery_json) {
|
||||||
|
this.sqlQueryText = item.sqlquery_json;
|
||||||
|
} else {
|
||||||
|
this.sqlQueryText = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse blending_lakeids string to array
|
||||||
|
const lakeIds = item.blending_lakeids.split(',').map(id => id.trim()).filter(id => id !== '');
|
||||||
|
|
||||||
|
// Fetch keys for each lake ID
|
||||||
|
lakeIds.forEach(lakeId => {
|
||||||
|
this.mainService.fetchBlendingKeys(Number(lakeId)).subscribe(
|
||||||
|
(data: any) => {
|
||||||
|
this.blendingKeysData.push({
|
||||||
|
lakeId: lakeId,
|
||||||
|
tableName: data.tableName,
|
||||||
|
headers: data.headers
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error fetching keys for lake ID:', lakeId, error);
|
||||||
|
this.toastr.error(`Failed to fetch keys for Data Lake ${lakeId}`);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to update SQL query
|
||||||
|
updateSqlQuery() {
|
||||||
|
if (!this.selectedBlendingItem) {
|
||||||
|
this.toastr.error('No blending item selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the SQL query JSON field
|
||||||
|
this.selectedBlendingItem.sqlquery_json = this.sqlQueryText;
|
||||||
|
|
||||||
|
// Call update service
|
||||||
|
this.mainService.update(this.selectedBlendingItem.id, this.selectedBlendingItem).subscribe(
|
||||||
|
(response) => {
|
||||||
|
this.toastr.success('SQL query updated successfully');
|
||||||
|
// Update the local data
|
||||||
|
const index = this.product.findIndex(p => p.id === this.selectedBlendingItem.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.product[index] = {...this.selectedBlendingItem};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error updating SQL query:', error);
|
||||||
|
this.toastr.error('Failed to update SQL query');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to close blending keys panel
|
||||||
|
closeBlendingKeys() {
|
||||||
|
this.showBlendingKeys = false;
|
||||||
|
this.blendingKeysData = [];
|
||||||
|
this.sqlQueryText = '';
|
||||||
|
this.selectedBlendingItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle checkbox change for blending lakes (ADD form)
|
||||||
|
onBlendingLakeCheckboxChange(event: any, lakeId: number) {
|
||||||
|
const lakeIdStr = lakeId.toString();
|
||||||
|
if (event.target.checked) {
|
||||||
|
// Add to selected blending lake IDs
|
||||||
|
if (!this.selectedBlendingLakeIds.includes(lakeIdStr)) {
|
||||||
|
this.selectedBlendingLakeIds.push(lakeIdStr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from selected blending lake IDs
|
||||||
|
const index = this.selectedBlendingLakeIds.indexOf(lakeIdStr);
|
||||||
|
if (index > -1) {
|
||||||
|
this.selectedBlendingLakeIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the form control value as a comma-separated string
|
||||||
|
this.entryForm.get('blending_lakeids')?.setValue(this.selectedBlendingLakeIds.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check if a blending lake is selected (ADD form)
|
||||||
|
isBlendingLakeSelected(lakeId: number): boolean {
|
||||||
|
return this.selectedBlendingLakeIds.includes(lakeId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle checkbox change for blending lakes (EDIT form)
|
||||||
|
onEditBlendingLakeCheckboxChange(event: any, lakeId: number) {
|
||||||
|
const lakeIdStr = lakeId.toString();
|
||||||
|
if (event.target.checked) {
|
||||||
|
// Add to selected blending lake IDs
|
||||||
|
if (!this.editSelectedBlendingLakeIds.includes(lakeIdStr)) {
|
||||||
|
this.editSelectedBlendingLakeIds.push(lakeIdStr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from selected blending lake IDs
|
||||||
|
const index = this.editSelectedBlendingLakeIds.indexOf(lakeIdStr);
|
||||||
|
if (index > -1) {
|
||||||
|
this.editSelectedBlendingLakeIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the rowSelected value as a comma-separated string
|
||||||
|
this.rowSelected.blending_lakeids = this.editSelectedBlendingLakeIds.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check if a blending lake is selected (EDIT form)
|
||||||
|
isEditBlendingLakeSelected(lakeId: number): boolean {
|
||||||
|
return this.editSelectedBlendingLakeIds.includes(lakeId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to open field mapping modal
|
||||||
|
openFieldMappingModal(item: any) {
|
||||||
|
this.selectedMappingItem = item;
|
||||||
|
this.fieldMappingData = [];
|
||||||
|
this.fieldMappings = {};
|
||||||
|
this.showFieldMappingModal = true;
|
||||||
|
|
||||||
|
// Fetch available keys using the same API as calculated fields
|
||||||
|
this.mainService.fetchAvailableKeys(item.url, item.sure_connect_id).subscribe(
|
||||||
|
(keys: string[]) => {
|
||||||
|
// Initialize field mappings with empty values
|
||||||
|
this.fieldMappingData = keys.map(key => ({
|
||||||
|
original: key,
|
||||||
|
mapped: ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If there's existing mapping data, populate it
|
||||||
|
if (item.mapping_json) {
|
||||||
|
try {
|
||||||
|
const existingMappings = JSON.parse(item.mapping_json);
|
||||||
|
this.fieldMappingData.forEach(field => {
|
||||||
|
if (existingMappings[field.original]) {
|
||||||
|
field.mapped = existingMappings[field.original];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing existing mapping JSON:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error fetching keys for field mapping:', error);
|
||||||
|
this.toastr.error('Failed to fetch available keys for field mapping');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to update field mappings
|
||||||
|
updateFieldMappings() {
|
||||||
|
if (!this.selectedMappingItem) {
|
||||||
|
this.toastr.error('No item selected for mapping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mapping object
|
||||||
|
const mappings: { [key: string]: string } = {};
|
||||||
|
this.fieldMappingData.forEach(field => {
|
||||||
|
if (field.mapped && field.mapped.trim() !== '') {
|
||||||
|
mappings[field.original] = field.mapped.trim();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to JSON string
|
||||||
|
const mappingJson = JSON.stringify(mappings);
|
||||||
|
|
||||||
|
// Update the mapping JSON field
|
||||||
|
this.selectedMappingItem.mapping_json = mappingJson;
|
||||||
|
|
||||||
|
// Call update service
|
||||||
|
this.mainService.update(this.selectedMappingItem.id, this.selectedMappingItem).subscribe(
|
||||||
|
(response) => {
|
||||||
|
this.toastr.success('Field mappings updated successfully');
|
||||||
|
// Update the local data
|
||||||
|
const index = this.product.findIndex(p => p.id === this.selectedMappingItem.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.product[index] = {...this.selectedMappingItem};
|
||||||
|
}
|
||||||
|
this.closeFieldMappingModal();
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error updating field mappings:', error);
|
||||||
|
this.toastr.error('Failed to update field mappings');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to close field mapping modal
|
||||||
|
closeFieldMappingModal() {
|
||||||
|
this.showFieldMappingModal = false;
|
||||||
|
this.fieldMappingData = [];
|
||||||
|
this.fieldMappings = {};
|
||||||
|
this.selectedMappingItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to update individual field mapping
|
||||||
|
updateFieldMapping(index: number, value: string) {
|
||||||
|
if (index >= 0 && index < this.fieldMappingData.length) {
|
||||||
|
this.fieldMappingData[index].mapped = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to open scheduler modal
|
||||||
|
openSchedulerModal(item: any) {
|
||||||
|
this.selectedSchedulerItem = item;
|
||||||
|
this.schedulerJob = null;
|
||||||
|
this.showSchedulerModal = true;
|
||||||
|
|
||||||
|
// Fetch job by lake ID
|
||||||
|
this.schedulerService.getJobByLakeId(item.id).subscribe(
|
||||||
|
(job: any) => {
|
||||||
|
this.schedulerJob = job;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// If job not found, it's expected - we'll show create option
|
||||||
|
if (error.status === 404) {
|
||||||
|
this.schedulerJob = null;
|
||||||
|
} else {
|
||||||
|
console.error('Error fetching scheduler job:', error);
|
||||||
|
this.toastr.error('Failed to fetch scheduler job information');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to create a new job
|
||||||
|
createJob() {
|
||||||
|
if (!this.selectedSchedulerItem) {
|
||||||
|
this.toastr.error('No data lake selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobData = {
|
||||||
|
name: this.selectedSchedulerItem.name,
|
||||||
|
status: 'RUNNING',
|
||||||
|
description: `Scheduled job for ${this.selectedSchedulerItem.name}`,
|
||||||
|
jobType: 'DATALAKE',
|
||||||
|
lakeid: this.selectedSchedulerItem.id
|
||||||
|
};
|
||||||
|
|
||||||
|
this.schedulerService.createJob(jobData).subscribe(
|
||||||
|
(job: any) => {
|
||||||
|
this.schedulerJob = job;
|
||||||
|
this.toastr.success('Job created successfully');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error creating job:', error);
|
||||||
|
this.toastr.error('Failed to create job');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to pause a job
|
||||||
|
pauseJob() {
|
||||||
|
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||||
|
this.toastr.error('No job selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulerService.pauseJob(this.schedulerJob.id).subscribe(
|
||||||
|
(response: any) => {
|
||||||
|
this.schedulerJob.status = 'PAUSED';
|
||||||
|
this.toastr.success('Job paused successfully');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error pausing job:', error);
|
||||||
|
this.toastr.error('Failed to pause job');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to resume a job
|
||||||
|
resumeJob() {
|
||||||
|
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||||
|
this.toastr.error('No job selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulerService.resumeJob(this.schedulerJob.id).subscribe(
|
||||||
|
(response: any) => {
|
||||||
|
this.schedulerJob.status = 'RUNNING';
|
||||||
|
this.toastr.success('Job resumed successfully');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error resuming job:', error);
|
||||||
|
this.toastr.error('Failed to resume job');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to stop a job
|
||||||
|
stopJob() {
|
||||||
|
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||||
|
this.toastr.error('No job selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.schedulerService.stopJob(this.schedulerJob.id).subscribe(
|
||||||
|
(response: any) => {
|
||||||
|
this.schedulerJob.status = 'STOPPED';
|
||||||
|
this.toastr.success('Job stopped successfully');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error stopping job:', error);
|
||||||
|
this.toastr.error('Failed to stop job');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to restart a stopped job
|
||||||
|
restartJob() {
|
||||||
|
if (!this.schedulerJob || !this.schedulerJob.id) {
|
||||||
|
this.toastr.error('No job selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To restart a job, we need to create a new job with the same parameters
|
||||||
|
const jobData = {
|
||||||
|
name: this.schedulerJob.name,
|
||||||
|
status: 'RUNNING',
|
||||||
|
description: this.schedulerJob.description,
|
||||||
|
jobType: this.schedulerJob.jobType,
|
||||||
|
lakeid: this.schedulerJob.lakeid
|
||||||
|
};
|
||||||
|
|
||||||
|
this.schedulerService.createJob(jobData).subscribe(
|
||||||
|
(job: any) => {
|
||||||
|
this.schedulerJob = job;
|
||||||
|
this.toastr.success('Job restarted successfully');
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Error restarting job:', error);
|
||||||
|
this.toastr.error('Failed to restart job');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to close scheduler modal
|
||||||
|
closeSchedulerModal() {
|
||||||
|
this.showSchedulerModal = false;
|
||||||
|
this.schedulerJob = null;
|
||||||
|
this.selectedSchedulerItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to get status badge class
|
||||||
|
getStatusBadgeClass(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'RUNNING':
|
||||||
|
return 'badge-success';
|
||||||
|
case 'PAUSED':
|
||||||
|
return 'badge-warning';
|
||||||
|
case 'STOPPED':
|
||||||
|
return 'badge-danger';
|
||||||
|
default:
|
||||||
|
return 'badge-light';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export class Data_lakeservice{
|
|||||||
return this.apiRequest.get(apiUrl);
|
return this.apiRequest.get(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to fetch blending keys for a specific lake ID
|
||||||
|
fetchBlendingKeys(lakeId: number): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/keys/${lakeId}`;
|
||||||
|
return this.apiRequest.get(_http);
|
||||||
|
}
|
||||||
|
|
||||||
// Method to update calculated fields for a data lake item
|
// Method to update calculated fields for a data lake item
|
||||||
updateCalculatedFields(id: number, calculatedFieldJson: string, isCalculatedField: boolean): Observable<any> {
|
updateCalculatedFields(id: number, calculatedFieldJson: string, isCalculatedField: boolean): Observable<any> {
|
||||||
const _http = this.baseURL + "/" + id;
|
const _http = this.baseURL + "/" + id;
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<div class="cron-job-builder">
|
<div class="cron-job-builder">
|
||||||
<h4>Cron Job Builder</h4>
|
<h4>Cron Job Builder</h4>
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
|
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Second</label>
|
||||||
|
<select class="form-control" [(ngModel)]="second" (change)="onFieldChange()">
|
||||||
|
<option *ngFor="let option of secondOptions" [value]="option.value">{{ option.label }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Minute</label>
|
<label>Minute</label>
|
||||||
@@ -28,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-lg-3 clr-col-md-6 clr-col-sm-6 clr-col-12">
|
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Month</label>
|
<label>Month</label>
|
||||||
<select class="form-control" [(ngModel)]="month" (change)="onFieldChange()">
|
<select class="form-control" [(ngModel)]="month" (change)="onFieldChange()">
|
||||||
@@ -37,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clr-col-lg-3 clr-col-md-6 clr-col-sm-12 clr-col-12">
|
<div class="clr-col-lg-2 clr-col-md-4 clr-col-sm-6 clr-col-12">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Day of Week</label>
|
<label>Day of Week</label>
|
||||||
<select class="form-control" [(ngModel)]="dayOfWeek" (change)="onFieldChange()">
|
<select class="form-control" [(ngModel)]="dayOfWeek" (change)="onFieldChange()">
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
@Input() instanceId: string = ''; // Unique identifier for each instance
|
@Input() instanceId: string = ''; // Unique identifier for each instance
|
||||||
@Output() cronExpressionChange = new EventEmitter<string>();
|
@Output() cronExpressionChange = new EventEmitter<string>();
|
||||||
|
|
||||||
// Cron job fields
|
// Cron job fields (now starting with seconds)
|
||||||
|
second: string = '*';
|
||||||
minute: string = '*';
|
minute: string = '*';
|
||||||
hour: string = '*';
|
hour: string = '*';
|
||||||
dayOfMonth: string = '*';
|
dayOfMonth: string = '*';
|
||||||
@@ -21,6 +22,7 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
cronDescription: string = '';
|
cronDescription: string = '';
|
||||||
|
|
||||||
// Options for selectors
|
// Options for selectors
|
||||||
|
secondOptions: any[] = [];
|
||||||
minuteOptions: any[] = [];
|
minuteOptions: any[] = [];
|
||||||
hourOptions: any[] = [];
|
hourOptions: any[] = [];
|
||||||
dayOfMonthOptions: any[] = [];
|
dayOfMonthOptions: any[] = [];
|
||||||
@@ -29,9 +31,10 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Initialize options for each instance
|
// Initialize options for each instance
|
||||||
this.minuteOptions = this.generateOptions(0, 59);
|
this.secondOptions = this.generateOptions(0, 59, 'second');
|
||||||
this.hourOptions = this.generateOptions(0, 23);
|
this.minuteOptions = this.generateOptions(0, 59, 'minute');
|
||||||
this.dayOfMonthOptions = this.generateOptions(1, 31);
|
this.hourOptions = this.generateOptions(0, 23, 'hour');
|
||||||
|
this.dayOfMonthOptions = this.generateOptions(1, 31, 'day');
|
||||||
this.monthOptions = [
|
this.monthOptions = [
|
||||||
{ value: '*', label: 'Every month' },
|
{ value: '*', label: 'Every month' },
|
||||||
{ value: '1', label: 'January' },
|
{ value: '1', label: 'January' },
|
||||||
@@ -73,8 +76,8 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
generateOptions(start: number, end: number): any[] {
|
generateOptions(start: number, end: number, type: string): any[] {
|
||||||
const options = [{ value: '*', label: `Every (${start === 0 ? '0' : start}-${end})` }];
|
const options = [{ value: '*', label: `Every ${type} (${start === 0 ? '0' : start}-${end})` }];
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
options.push({ value: i.toString(), label: i.toString() });
|
options.push({ value: i.toString(), label: i.toString() });
|
||||||
}
|
}
|
||||||
@@ -83,7 +86,16 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
parseCronExpression(expression: string) {
|
parseCronExpression(expression: string) {
|
||||||
const parts = expression.split(' ');
|
const parts = expression.split(' ');
|
||||||
if (parts.length >= 5) {
|
if (parts.length >= 6) { // Now expecting 6 parts with seconds
|
||||||
|
this.second = parts[0];
|
||||||
|
this.minute = parts[1];
|
||||||
|
this.hour = parts[2];
|
||||||
|
this.dayOfMonth = parts[3];
|
||||||
|
this.month = parts[4];
|
||||||
|
this.dayOfWeek = parts[5];
|
||||||
|
} else if (parts.length >= 5) { // For backward compatibility with 5-part cron expressions
|
||||||
|
// Default seconds to 0 if not provided
|
||||||
|
this.second = '0';
|
||||||
this.minute = parts[0];
|
this.minute = parts[0];
|
||||||
this.hour = parts[1];
|
this.hour = parts[1];
|
||||||
this.dayOfMonth = parts[2];
|
this.dayOfMonth = parts[2];
|
||||||
@@ -95,19 +107,33 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
generateDescription() {
|
generateDescription() {
|
||||||
let description = 'Runs ';
|
let description = 'Runs ';
|
||||||
|
|
||||||
|
// Second description
|
||||||
|
if (this.second === '*') {
|
||||||
|
description += 'every second';
|
||||||
|
} else if (this.second.includes('/')) {
|
||||||
|
const interval = this.second.split('/')[1];
|
||||||
|
description += `every ${interval} seconds`;
|
||||||
|
} else {
|
||||||
|
description += `at second ${this.second}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Minute description
|
// Minute description
|
||||||
if (this.minute === '*') {
|
if (this.minute === '*') {
|
||||||
description += 'every minute';
|
if (this.second === '*') {
|
||||||
|
description += '';
|
||||||
|
} else {
|
||||||
|
description += ' of every minute';
|
||||||
|
}
|
||||||
} else if (this.minute.includes('/')) {
|
} else if (this.minute.includes('/')) {
|
||||||
const interval = this.minute.split('/')[1];
|
const interval = this.minute.split('/')[1];
|
||||||
description += `every ${interval} minutes`;
|
description += ` of every ${interval} minutes`;
|
||||||
} else {
|
} else {
|
||||||
description += `at minute ${this.minute}`;
|
description += ` at minute ${this.minute}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hour description
|
// Hour description
|
||||||
if (this.hour === '*') {
|
if (this.hour === '*') {
|
||||||
if (this.minute === '*') {
|
if (this.minute === '*' && this.second === '*') {
|
||||||
description += '';
|
description += '';
|
||||||
} else {
|
} else {
|
||||||
description += ' past every hour';
|
description += ' past every hour';
|
||||||
@@ -146,21 +172,23 @@ export class CronJobBuilderComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Special case for simple patterns
|
// Special case for simple patterns
|
||||||
if (this.minute === '0' && this.hour === '0' && this.dayOfMonth === '1' && this.month === '*' && this.dayOfWeek === '*') {
|
if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '1' && this.month === '*' && this.dayOfWeek === '*') {
|
||||||
description = 'Runs at midnight on the first day of every month';
|
description = 'Runs at midnight on the first day of every month';
|
||||||
} else if (this.minute === '0' && this.hour === '0' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
} else if (this.second === '0' && this.minute === '0' && this.hour === '0' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||||
description = 'Runs at midnight every day';
|
description = 'Runs at midnight every day';
|
||||||
} else if (this.minute === '0' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
} else if (this.second === '0' && this.minute === '0' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||||
description = 'Runs at the top of every hour';
|
description = 'Runs at the top of every hour';
|
||||||
} else if (this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
} else if (this.second === '0' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||||
description = 'Runs every minute';
|
description = 'Runs at the top of every minute';
|
||||||
|
} else if (this.second === '*' && this.minute === '*' && this.hour === '*' && this.dayOfMonth === '*' && this.month === '*' && this.dayOfWeek === '*') {
|
||||||
|
description = 'Runs every second';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cronDescription = description;
|
this.cronDescription = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCronExpression() {
|
buildCronExpression() {
|
||||||
const expression = `${this.minute} ${this.hour} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
|
const expression = `${this.second} ${this.minute} ${this.hour} ${this.dayOfMonth} ${this.month} ${this.dayOfWeek}`;
|
||||||
this.cronExpressionChange.emit(expression);
|
this.cronExpressionChange.emit(expression);
|
||||||
this.generateDescription(); // Update description when expression changes
|
this.generateDescription(); // Update description when expression changes
|
||||||
return expression;
|
return expression;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
import { HttpClient } from "@angular/common/http";
|
||||||
|
import { ApiRequestService } from "src/app/services/api/api-request.service";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DataLakeSchedulerService {
|
||||||
|
private baseURL = "scheduler";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private apiRequest: ApiRequestService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
// Get job by lake ID
|
||||||
|
getJobByLakeId(lakeId: number): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/lake/${lakeId}`;
|
||||||
|
return this.apiRequest.get(_http);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new job
|
||||||
|
createJob(jobData: any): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/create`;
|
||||||
|
return this.apiRequest.post(_http, jobData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause a job
|
||||||
|
pauseJob(jobId: number): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/pause/${jobId}`;
|
||||||
|
return this.apiRequest.post(_http, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume a job
|
||||||
|
resumeJob(jobId: number): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/resume/${jobId}`;
|
||||||
|
return this.apiRequest.post(_http, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop a job
|
||||||
|
stopJob(jobId: number): Observable<any> {
|
||||||
|
const _http = `${this.baseURL}/stop/${jobId}`;
|
||||||
|
return this.apiRequest.delete(_http);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +1,67 @@
|
|||||||
<ol class="breadcrumb breadcrumb-arrow font-trirong">
|
<ol class="breadcrumb breadcrumb-arrow font-trirong">
|
||||||
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
<li><a href="javascript://" [routerLink]="['/cns-portal/dashboard/order']"><clr-icon shape="home"></clr-icon></a></li>
|
||||||
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
<li><a href="javascript://"><clr-icon shape="dashboard"></clr-icon> {{ 'Dashboard_builder' | translate }}</a></li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="dg-wrapper">
|
<div class="dg-wrapper">
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<div class="clr-col-8">
|
<div class="clr-col-8">x
|
||||||
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
<h3>{{ 'Dashboard_builder' | translate }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="clr-col-4" style="text-align: right;">
|
<div class="clr-col-4" style="text-align: right;">
|
||||||
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
<button class="btn btn-success" [routerLink]="['/cns-portal/shield-dashboard']">
|
||||||
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
<clr-icon shape="shield"></clr-icon>Shield Dashboard
|
||||||
</button>
|
</button>
|
||||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||||
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
<clr-icon shape="grid-view"></clr-icon>{{ 'Dashboard_runner' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline" (click)="onExport()">
|
<!-- Add Chart Config button -->
|
||||||
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
<button class="btn btn-primary" (click)="openChartConfig()">
|
||||||
</button>
|
<clr-icon shape="cog"></clr-icon> Chart Config
|
||||||
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
</button>
|
||||||
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
<button class="btn btn-outline" (click)="onExport()">
|
||||||
</button>
|
<clr-icon shape="export"></clr-icon> {{ 'EXPORT_XLSX' | translate }}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
<button id="add" class="btn btn-primary" (click)="gotoadd()">
|
||||||
|
<clr-icon shape="plus"></clr-icon> {{ 'ADD' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<clr-datagrid [clrDgLoading]="loading">
|
<clr-datagrid [clrDgLoading]="loading">
|
||||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ...... </clr-spinner></ng-template>
|
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{ 'Loading' | translate }} ......
|
||||||
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
|
</clr-spinner></ng-template>
|
||||||
|
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||||
|
</clr-dg-placeholder>
|
||||||
|
|
||||||
<clr-dg-column [clrDgField]="''">
|
<clr-dg-column [clrDgField]="''">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Go_to' | translate }}
|
{{ 'Go_to' | translate }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column>
|
</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'dashboard_name'">
|
<clr-dg-column [clrDgField]="'dashboard_name'">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Dashboard_Name' | translate }}
|
{{ 'Dashboard_Name' | translate }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column>
|
</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'description'">
|
<clr-dg-column [clrDgField]="'description'">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Description' | translate }}
|
{{ 'Description' | translate }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column >
|
</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
<clr-dg-column [clrDgField]="'secuirity_profile'">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Security_Profile' | translate }}
|
{{ 'Security_Profile' | translate }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column >
|
</clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'add_to_home'">
|
<clr-dg-column [clrDgField]="'add_to_home'">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Add_to_home' | translate }}
|
{{ 'Add_to_home' | translate }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column >
|
</clr-dg-column>
|
||||||
<!-- <clr-dg-column [clrDgField]="'formType'">
|
<!-- <clr-dg-column [clrDgField]="'formType'">
|
||||||
<ng-container *clrDgHideableColumn="{hidden: false}">
|
<ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
Build Status
|
Build Status
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@@ -65,81 +71,86 @@
|
|||||||
Testing
|
Testing
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</clr-dg-column > -->
|
</clr-dg-column > -->
|
||||||
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="'action'"> <ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{ 'Action' | translate }}
|
{{ 'Action' | translate }}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
<clr-dg-row *clrDgItems="let user of data?.slice()?.reverse();" [clrDgItem]="user">
|
||||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
|
||||||
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
(click)="goToEdit(user.id)"> {{ 'SET_UP' | translate }}</span></clr-dg-cell>
|
||||||
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
<clr-dg-cell>{{user.dashboard_name}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
<clr-dg-cell>{{user.description}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
<clr-dg-cell>{{user.secuirity_profile}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{user.add_to_home}}</clr-dg-cell>
|
||||||
|
|
||||||
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
<!-- <clr-dg-cell><input type="radio" id="cb1" class="dots" [ngStyle]="{'background-color': user.build == true ? 'green' : 'red'}"></clr-dg-cell>
|
||||||
<clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
|
<clr-dg-cell>{{user.testing}}</clr-dg-cell> -->
|
||||||
<clr-dg-cell>
|
<clr-dg-cell>
|
||||||
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
<!-- <span style="cursor: pointer; padding: 10px; "><clr-icon shape="form" (click)="goToEdit(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
|
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
|
||||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
|
class="tooltip tooltip-sm tooltip-top-left">
|
||||||
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
|
||||||
</a>
|
style="color: red;"></clr-icon></span>
|
||||||
|
<span class="tooltip-content">{{ 'Delete' | translate }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<clr-signpost>
|
<clr-signpost>
|
||||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
|
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||||
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||||
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
<h5 style="margin-top: 0">{{ 'Who_Column' | translate }}</h5>
|
||||||
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
<div>{{ 'Account_ID' | translate }}: <code class="clr-code">{{ user.accountId }}</code></div>
|
||||||
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
<div>{{ 'Created_At' | translate }}: <code class="clr-code">{{ user.createdAt | date }}</code></div>
|
||||||
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
<div>{{ 'Created_By' | translate }}: <code class="clr-code">{{ user.createdBy }}</code></div>
|
||||||
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
<div>{{ 'Updated_At' | translate }}: <code class="clr-code">{{ user.updatedAt | date }}</code></div>
|
||||||
</clr-signpost-content>
|
<div>{{ 'Updated_By' | translate }}: <code class="clr-code">{{ user.updatedBy }}</code></div>
|
||||||
</clr-signpost>
|
</clr-signpost-content>
|
||||||
|
</clr-signpost>
|
||||||
|
|
||||||
</clr-dg-cell>
|
</clr-dg-cell>
|
||||||
<clr-dg-action-overflow>
|
<clr-dg-action-overflow>
|
||||||
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit" class="is-error"></clr-icon></button>
|
<button class="action-item" (click)="goToEditData(user.id)">{{ 'Edit' | translate }} <clr-icon shape="edit"
|
||||||
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
class="is-error"></clr-icon></button>
|
||||||
</clr-dg-action-overflow>
|
<!-- <button class="action-item" (click)="onDelete(user)">Delete<clr-icon shape="trash" class="is-error"></clr-icon></button> -->
|
||||||
</clr-dg-row>
|
</clr-dg-action-overflow>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{ 'Users_per_page' | translate }}</clr-dg-page-size>
|
||||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||||
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
{{ 'of' | translate }} {{pagination.totalItems}} {{ 'users' | translate }}
|
||||||
</clr-dg-pagination>
|
</clr-dg-pagination>
|
||||||
</clr-dg-footer>
|
</clr-dg-footer>
|
||||||
</clr-datagrid>
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="s-order-dash-pg">
|
||||||
|
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
||||||
|
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
||||||
|
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
||||||
|
</div>
|
||||||
|
<div class="chart-box" id="word1"><br>
|
||||||
|
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
||||||
|
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
||||||
|
</div>
|
||||||
|
<div class="chart-box" id="word1"><br>
|
||||||
|
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
||||||
|
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-modal>
|
||||||
|
|
||||||
<clr-modal [(clrModalOpen)]="addModall" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||||
<div class="modal-body">
|
<div class="modal-body" *ngIf="rowSelected.id">
|
||||||
<div class="s-order-dash-pg">
|
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
||||||
<div class="chart-box" id="word1" (click)="gotoadd()"><br>
|
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||||
<img style="margin: auto; display: block;" src="/assets/images/fromscratch.png" height="90" width="90">
|
<div class="modal-footer">
|
||||||
<h5 class="center"> <b>{{ 'Start_from_scratch' | translate }}</b> </h5>
|
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
||||||
</div>
|
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{ 'Delete' | translate }}</button>
|
||||||
<div class="chart-box" id="word1" ><br>
|
</div>
|
||||||
<img style="margin: auto; display: block;" src="/assets/images/copytemplate.png" height="90" width="90">
|
</div>
|
||||||
<h5 class="center"> <b>{{ 'Import_from_template' | translate }}</b> </h5>
|
</clr-modal>
|
||||||
</div>
|
|
||||||
<div class="chart-box" id="word1"><br>
|
|
||||||
<img style="margin: auto; display: block;" src="/assets/images/database.png" height="90" width="90">
|
|
||||||
<h5 class="center"> <b>{{ 'Import_from_public_project' | translate }}</b> </h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</clr-modal>
|
|
||||||
|
|
||||||
<clr-modal [(clrModalOpen)]="modalDelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
|
||||||
<div class="modal-body" *ngIf="rowSelected.id">
|
|
||||||
<h1 class="delete">{{ 'Are_you_sure_want_to_delete' | translate }}</h1>
|
|
||||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-outline" (click)="modalDelete = false">{{ 'Cancel' | translate }}</button>
|
|
||||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{ 'Delete' | translate }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</clr-modal>
|
|
||||||
@@ -24,6 +24,7 @@ export class AllnewdashComponent implements OnInit {
|
|||||||
projectname;
|
projectname;
|
||||||
projectId;
|
projectId;
|
||||||
error;
|
error;
|
||||||
|
chartConfigManagerOpen = false;
|
||||||
constructor(
|
constructor(
|
||||||
private router : Router,
|
private router : Router,
|
||||||
private route: ActivatedRoute,private dashboardService : DashboardService,
|
private route: ActivatedRoute,private dashboardService : DashboardService,
|
||||||
@@ -121,4 +122,9 @@ export class AllnewdashComponent implements OnInit {
|
|||||||
// this.router.navigate(['../editdashn'],{relativeTo:this.route});
|
// this.router.navigate(['../editdashn'],{relativeTo:this.route});
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Add method to open chart configuration manager
|
||||||
|
openChartConfig(): void {
|
||||||
|
this.router.navigate(['../chart-types'],{relativeTo:this.route});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,630 @@
|
|||||||
|
<div class="chart-config-manager">
|
||||||
|
<h2>Chart Configuration Manager</h2>
|
||||||
|
|
||||||
|
<!-- Chart Types Section -->
|
||||||
|
<clr-tabs>
|
||||||
|
<clr-tab>
|
||||||
|
<button clrTabLink>Chart Types</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Chart Types</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
|
||||||
|
Add Chart Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Chart Type Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddChartTypeForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newChartType.name" name="chartTypeName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newChartType.displayName" name="chartTypeDisplayName" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newChartType.description" name="chartTypeDescription"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Active</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newChartType.isActive" name="chartTypeIsActive" />
|
||||||
|
<label>Active</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createChartType()" [disabled]="!newChartType.name">Save</button>
|
||||||
|
<button class="btn" (click)="showAddChartTypeForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Types Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid>
|
||||||
|
<clr-dg-column>Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Description</clr-dg-column>
|
||||||
|
<clr-dg-column>Status</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||||
|
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||||
|
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)">
|
||||||
|
<cds-icon shape="eye"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTypes.length}} chart types
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Chart Type Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedChartType">
|
||||||
|
<h4>Edit Chart Type</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedChartType.name" name="editChartTypeName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedChartType.displayName" name="editChartTypeDisplayName" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedChartType.description" name="editChartTypeDescription"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Active</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartType.isActive" name="editChartTypeIsActive" />
|
||||||
|
<label>Active</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateChartType()" [disabled]="!selectedChartType.name">Update</button>
|
||||||
|
<button class="btn" (click)="selectedChartType = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- UI Components Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>UI Components</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>UI Components for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
|
||||||
|
Add UI Component
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add UI Component Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddUiComponentForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Placeholder</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createUiComponent()" [disabled]="!newUiComponent.componentName">Save</button>
|
||||||
|
<button class="btn" (click)="showAddUiComponentForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UI Components Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid>
|
||||||
|
<clr-dg-column>Component Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Component Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Placeholder</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Sort Order</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||||
|
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.placeholder}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||||
|
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.sortOrder}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{uiComponents.length}} UI components
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit UI Component Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedUiComponent">
|
||||||
|
<h4>Edit UI Component</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Placeholder</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateUiComponent()" [disabled]="!selectedUiComponent.componentName">Update</button>
|
||||||
|
<button class="btn" (click)="selectedUiComponent = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Component Properties Section -->
|
||||||
|
<clr-tab *ngIf="selectedUiComponent">
|
||||||
|
<button clrTabLink>Component Properties</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||||
|
Add Property
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Value</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createComponentProperty()" [disabled]="!newComponentProperty.propertyName">Save</button>
|
||||||
|
<button class="btn" (click)="showAddComponentPropertyForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Component Properties Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid>
|
||||||
|
<clr-dg-column>Property Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Value</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||||
|
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{componentProperties.length}} properties
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||||
|
<h4>Edit Component Property</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Value</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateComponentProperty()" [disabled]="!selectedComponentProperty.propertyName">Update</button>
|
||||||
|
<button class="btn" (click)="selectedComponentProperty = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Chart Templates Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>Chart Templates</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Templates for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
|
||||||
|
Add Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Chart Template Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddChartTemplateForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Template Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>HTML Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>CSS Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Default</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
|
||||||
|
<label>Default Template</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createChartTemplate()" [disabled]="!newChartTemplate.templateName">Save</button>
|
||||||
|
<button class="btn" (click)="showAddChartTemplateForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Templates Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid>
|
||||||
|
<clr-dg-column>Template Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Is Default</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
|
||||||
|
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
|
||||||
|
{{template.isDefault ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTemplates.length}} templates
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Chart Template Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedChartTemplate">
|
||||||
|
<h4>Edit Chart Template</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Template Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>HTML Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>CSS Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Default</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
|
||||||
|
<label>Default Template</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateChartTemplate()" [disabled]="!selectedChartTemplate.templateName">Update</button>
|
||||||
|
<button class="btn" (click)="selectedChartTemplate = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Dynamic Fields Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>Dynamic Fields</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
|
||||||
|
Add Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Dynamic Field Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddDynamicFieldForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||||
|
<label>Show in UI</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName">Save</button>
|
||||||
|
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic Fields Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid>
|
||||||
|
<clr-dg-column>Field Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Show in UI</clr-dg-column>
|
||||||
|
<clr-dg-column>Sort Order</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
|
||||||
|
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
|
||||||
|
{{field.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
|
||||||
|
{{field.showInUi ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{dynamicFields.length}} fields
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Dynamic Field Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedDynamicField">
|
||||||
|
<h4>Edit Dynamic Field</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||||
|
<label>Show in UI</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName">Update</button>
|
||||||
|
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
</clr-tabs>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
.chart-config-manager {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-success {
|
||||||
|
background-color: #318700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-danger {
|
||||||
|
background-color: #e62200;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
|
||||||
|
interface ChartType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UiComponent {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
componentName: string;
|
||||||
|
componentType: string;
|
||||||
|
displayLabel: string;
|
||||||
|
placeholder: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentProperty {
|
||||||
|
id: number;
|
||||||
|
component: UiComponent;
|
||||||
|
propertyName: string;
|
||||||
|
propertyValue: string;
|
||||||
|
propertyType: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTemplate {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
templateName: string;
|
||||||
|
templateHtml: string;
|
||||||
|
templateCss: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicField {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
fieldName: string;
|
||||||
|
fieldLabel: string;
|
||||||
|
fieldType: string;
|
||||||
|
fieldOptions: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
showInUi: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-config-manager',
|
||||||
|
templateUrl: './chart-config-manager.component.html',
|
||||||
|
styleUrls: ['./chart-config-manager.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartConfigManagerComponent implements OnInit {
|
||||||
|
// Chart Types
|
||||||
|
chartTypes: ChartType[] = [];
|
||||||
|
selectedChartType: ChartType | null = null;
|
||||||
|
newChartType: Partial<ChartType> = {};
|
||||||
|
showAddChartTypeForm = false;
|
||||||
|
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// UI Components
|
||||||
|
uiComponents: UiComponent[] = [];
|
||||||
|
selectedUiComponent: UiComponent | null = null;
|
||||||
|
newUiComponent: Partial<UiComponent> = {};
|
||||||
|
showAddUiComponentForm = false;
|
||||||
|
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Component Properties
|
||||||
|
componentProperties: ComponentProperty[] = [];
|
||||||
|
selectedComponentProperty: ComponentProperty | null = null;
|
||||||
|
newComponentProperty: Partial<ComponentProperty> = {};
|
||||||
|
showAddComponentPropertyForm = false;
|
||||||
|
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Chart Templates
|
||||||
|
chartTemplates: ChartTemplate[] = [];
|
||||||
|
selectedChartTemplate: ChartTemplate | null = null;
|
||||||
|
newChartTemplate: Partial<ChartTemplate> = {};
|
||||||
|
showAddChartTemplateForm = false;
|
||||||
|
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Dynamic Fields
|
||||||
|
dynamicFields: DynamicField[] = [];
|
||||||
|
selectedDynamicField: DynamicField | null = null;
|
||||||
|
newDynamicField: Partial<DynamicField> = {};
|
||||||
|
showAddDynamicFieldForm = false;
|
||||||
|
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// API base URL
|
||||||
|
private apiUrl = environment.apiUrl || 'http://localhost:8080/api';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadChartTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart Type Methods
|
||||||
|
loadChartTypes(): void {
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.get<ChartType[]>(`${this.apiUrl}/chart-types`).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTypes = data;
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart types:', error);
|
||||||
|
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartType(): void {
|
||||||
|
if (!this.newChartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.post<ChartType>(`${this.apiUrl}/chart-types`, this.newChartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTypes.push(data);
|
||||||
|
this.newChartType = {};
|
||||||
|
this.showAddChartTypeForm = false;
|
||||||
|
this.showSuccess('Chart type created successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart type:', error);
|
||||||
|
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartType(): void {
|
||||||
|
if (!this.selectedChartType || !this.selectedChartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.put<ChartType>(`${this.apiUrl}/chart-types/${this.selectedChartType.id}`, this.selectedChartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.chartTypes[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.showSuccess('Chart type updated successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart type:', error);
|
||||||
|
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartType(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart type?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.delete(`${this.apiUrl}/chart-types/${id}`).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||||
|
// Also clear related data if the deleted chart type was selected
|
||||||
|
if (this.selectedChartType && this.selectedChartType.id === id) {
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.uiComponents = [];
|
||||||
|
this.chartTemplates = [];
|
||||||
|
this.dynamicFields = [];
|
||||||
|
}
|
||||||
|
this.showSuccess('Chart type deleted successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart type:', error);
|
||||||
|
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChartTypeForEdit(chartType: ChartType): void {
|
||||||
|
this.selectedChartType = { ...chartType };
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Component Methods
|
||||||
|
loadUiComponents(chartTypeId: number): void {
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.get<UiComponent[]>(`${this.apiUrl}/ui-components/chart-type/${chartTypeId}`).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents = data;
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading UI components:', error);
|
||||||
|
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createUiComponent(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
const uiComponentData = {
|
||||||
|
...this.newUiComponent,
|
||||||
|
chartType: { id: this.selectedChartType.id }
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post<UiComponent>(`${this.apiUrl}/ui-components`, uiComponentData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents.push(data);
|
||||||
|
this.newUiComponent = {};
|
||||||
|
this.showAddUiComponentForm = false;
|
||||||
|
this.showSuccess('UI component created successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating UI component:', error);
|
||||||
|
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUiComponent(): void {
|
||||||
|
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.put<UiComponent>(`${this.apiUrl}/ui-components/${this.selectedUiComponent.id}`, this.selectedUiComponent).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.uiComponents[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
this.showSuccess('UI component updated successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating UI component:', error);
|
||||||
|
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUiComponent(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.delete(`${this.apiUrl}/ui-components/${id}`).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||||
|
if (this.selectedUiComponent && this.selectedUiComponent.id === id) {
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
}
|
||||||
|
this.showSuccess('UI component deleted successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting UI component:', error);
|
||||||
|
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||||
|
this.selectedUiComponent = { ...uiComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component Property Methods
|
||||||
|
loadComponentProperties(componentId: number): void {
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.get<ComponentProperty[]>(`${this.apiUrl}/component-properties/component/${componentId}`).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties = data;
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading component properties:', error);
|
||||||
|
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createComponentProperty(): void {
|
||||||
|
if (!this.selectedUiComponent) {
|
||||||
|
this.showError('Please select a UI component first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
const componentPropertyData = {
|
||||||
|
...this.newComponentProperty,
|
||||||
|
component: { id: this.selectedUiComponent.id }
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post<ComponentProperty>(`${this.apiUrl}/component-properties`, componentPropertyData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties.push(data);
|
||||||
|
this.newComponentProperty = {};
|
||||||
|
this.showAddComponentPropertyForm = false;
|
||||||
|
this.showSuccess('Component property created successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating component property:', error);
|
||||||
|
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComponentProperty(): void {
|
||||||
|
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.put<ComponentProperty>(`${this.apiUrl}/component-properties/${this.selectedComponentProperty.id}`, this.selectedComponentProperty).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.componentProperties[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
this.showSuccess('Component property updated successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating component property:', error);
|
||||||
|
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteComponentProperty(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.delete(`${this.apiUrl}/component-properties/${id}`).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||||
|
if (this.selectedComponentProperty && this.selectedComponentProperty.id === id) {
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
}
|
||||||
|
this.showSuccess('Component property deleted successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting component property:', error);
|
||||||
|
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||||
|
this.selectedComponentProperty = { ...componentProperty };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart Template Methods
|
||||||
|
loadChartTemplates(chartTypeId: number): void {
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.get<ChartTemplate[]>(`${this.apiUrl}/chart-templates/chart-type/${chartTypeId}`).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates = data;
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart templates:', error);
|
||||||
|
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartTemplate(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
const chartTemplateData = {
|
||||||
|
...this.newChartTemplate,
|
||||||
|
chartType: { id: this.selectedChartType.id }
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post<ChartTemplate>(`${this.apiUrl}/chart-templates`, chartTemplateData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates.push(data);
|
||||||
|
this.newChartTemplate = {};
|
||||||
|
this.showAddChartTemplateForm = false;
|
||||||
|
this.showSuccess('Chart template created successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart template:', error);
|
||||||
|
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartTemplate(): void {
|
||||||
|
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.put<ChartTemplate>(`${this.apiUrl}/chart-templates/${this.selectedChartTemplate.id}`, this.selectedChartTemplate).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.chartTemplates[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
this.showSuccess('Chart template updated successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart template:', error);
|
||||||
|
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartTemplate(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.delete(`${this.apiUrl}/chart-templates/${id}`).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||||
|
if (this.selectedChartTemplate && this.selectedChartTemplate.id === id) {
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
}
|
||||||
|
this.showSuccess('Chart template deleted successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart template:', error);
|
||||||
|
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||||
|
this.selectedChartTemplate = { ...chartTemplate };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic Field Methods
|
||||||
|
loadDynamicFields(chartTypeId: number): void {
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.get<DynamicField[]>(`${this.apiUrl}/dynamic-fields/chart-type/${chartTypeId}`).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields = data;
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading dynamic fields:', error);
|
||||||
|
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createDynamicField(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
const dynamicFieldData = {
|
||||||
|
...this.newDynamicField,
|
||||||
|
chartType: { id: this.selectedChartType.id }
|
||||||
|
};
|
||||||
|
|
||||||
|
this.http.post<DynamicField>(`${this.apiUrl}/dynamic-fields`, dynamicFieldData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields.push(data);
|
||||||
|
this.newDynamicField = {};
|
||||||
|
this.showAddDynamicFieldForm = false;
|
||||||
|
this.showSuccess('Dynamic field created successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating dynamic field:', error);
|
||||||
|
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDynamicField(): void {
|
||||||
|
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.put<DynamicField>(`${this.apiUrl}/dynamic-fields/${this.selectedDynamicField.id}`, this.selectedDynamicField).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.dynamicFields[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
this.showSuccess('Dynamic field updated successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating dynamic field:', error);
|
||||||
|
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDynamicField(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.http.delete(`${this.apiUrl}/dynamic-fields/${id}`).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||||
|
if (this.selectedDynamicField && this.selectedDynamicField.id === id) {
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
}
|
||||||
|
this.showSuccess('Dynamic field deleted successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting dynamic field:', error);
|
||||||
|
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||||
|
this.selectedDynamicField = { ...dynamicField };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
onChartTypeSelect(chartType: ChartType): void {
|
||||||
|
this.selectedChartType = chartType;
|
||||||
|
this.loadUiComponents(chartType.id);
|
||||||
|
this.loadChartTemplates(chartType.id);
|
||||||
|
this.loadDynamicFields(chartType.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForms(): void {
|
||||||
|
this.newChartType = {};
|
||||||
|
this.newUiComponent = {};
|
||||||
|
this.newComponentProperty = {};
|
||||||
|
this.newChartTemplate = {};
|
||||||
|
this.newDynamicField = {};
|
||||||
|
this.showAddChartTypeForm = false;
|
||||||
|
this.showAddUiComponentForm = false;
|
||||||
|
this.showAddComponentPropertyForm = false;
|
||||||
|
this.showAddChartTemplateForm = false;
|
||||||
|
this.showAddDynamicFieldForm = false;
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
<div class="chart-config-manager">
|
||||||
|
<h2>Chart Configuration Manager</h2>
|
||||||
|
|
||||||
|
<!-- Test message to verify component is loading -->
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">Chart Config Manager Component Loaded Successfully</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Types Section -->
|
||||||
|
<clr-tab>
|
||||||
|
<button clrTabLink>Chart Types</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Chart Types</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddChartTypeForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Chart Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Chart Type Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddChartTypeForm">
|
||||||
|
<app-chart-type-form
|
||||||
|
[chartType]="newChartType"
|
||||||
|
(save)="createChartType()"
|
||||||
|
(cancel)="showAddChartTypeForm = false">
|
||||||
|
</app-chart-type-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Types Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Description</clr-dg-column>
|
||||||
|
<clr-dg-column>Status</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||||
|
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||||
|
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectChartTypeForEdit(chartType)" title="Edit" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="onChartTypeSelect(chartType)" title="View Details" [disabled]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="chartTypeLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="chartTypeLoadingState !== ClrLoadingState.LOADING" shape="eye"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTypes.length}} chart type(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Chart Type Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedChartType">
|
||||||
|
<h4>Edit Chart Type</h4>
|
||||||
|
<app-chart-type-form
|
||||||
|
[chartType]="selectedChartType"
|
||||||
|
[isEdit]="true"
|
||||||
|
(save)="updateChartType()"
|
||||||
|
(cancel)="selectedChartType = null">
|
||||||
|
</app-chart-type-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- UI Components Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>UI Components</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>UI Components for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddUiComponentForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add UI Component
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add UI Component Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddUiComponentForm">
|
||||||
|
<app-ui-component-form
|
||||||
|
[uiComponent]="newUiComponent"
|
||||||
|
(save)="createUiComponent()"
|
||||||
|
(cancel)="showAddUiComponentForm = false">
|
||||||
|
</app-ui-component-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UI Components Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Component Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Component Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||||
|
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||||
|
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell class="action-cell">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="onUiComponentSelect(uiComponent)" title="View Properties" [disabled]="uiComponentLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="uiComponentLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="uiComponentLoadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{uiComponents.length}} UI component(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit UI Component Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedUiComponent">
|
||||||
|
<h4>Edit UI Component</h4>
|
||||||
|
<app-ui-component-form
|
||||||
|
[uiComponent]="selectedUiComponent"
|
||||||
|
[isEdit]="true"
|
||||||
|
(save)="updateUiComponent()"
|
||||||
|
(cancel)="selectedUiComponent = null">
|
||||||
|
</app-ui-component-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Component Properties Section -->
|
||||||
|
<clr-tab *ngIf="selectedUiComponent">
|
||||||
|
<button clrTabLink>Component Properties</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Property
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||||
|
<app-component-property-form
|
||||||
|
[componentProperty]="newComponentProperty"
|
||||||
|
(save)="createComponentProperty()"
|
||||||
|
(cancel)="showAddComponentPropertyForm = false">
|
||||||
|
</app-component-property-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Component Properties Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Property Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Value</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||||
|
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{componentProperties.length}} propertie(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||||
|
<h4>Edit Component Property</h4>
|
||||||
|
<app-component-property-form
|
||||||
|
[componentProperty]="selectedComponentProperty"
|
||||||
|
[isEdit]="true"
|
||||||
|
(save)="updateComponentProperty()"
|
||||||
|
(cancel)="selectedComponentProperty = null">
|
||||||
|
</app-component-property-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Chart Templates Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>Chart Templates</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Templates for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddChartTemplateForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Chart Template Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddChartTemplateForm">
|
||||||
|
<app-chart-template-form
|
||||||
|
[chartTemplate]="newChartTemplate"
|
||||||
|
(save)="createChartTemplate()"
|
||||||
|
(cancel)="showAddChartTemplateForm = false">
|
||||||
|
</app-chart-template-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Templates Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Template Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Is Default</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let template of chartTemplates" [clrDgItem]="template">
|
||||||
|
<clr-dg-cell>{{template.templateName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="template.isDefault" [class.label-danger]="!template.isDefault">
|
||||||
|
{{template.isDefault ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{template.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{template.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(template)" title="Edit" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(template.id)" title="Delete" [disabled]="chartTemplateLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="chartTemplateLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="chartTemplateLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTemplates.length}} template(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Chart Template Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedChartTemplate">
|
||||||
|
<h4>Edit Chart Template</h4>
|
||||||
|
<app-chart-template-form
|
||||||
|
[chartTemplate]="selectedChartTemplate"
|
||||||
|
[isEdit]="true"
|
||||||
|
(save)="updateChartTemplate()"
|
||||||
|
(cancel)="selectedChartTemplate = null">
|
||||||
|
</app-chart-template-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
|
||||||
|
<!-- Dynamic Fields Section -->
|
||||||
|
<clr-tab *ngIf="selectedChartType">
|
||||||
|
<button clrTabLink>Dynamic Fields</button>
|
||||||
|
<clr-tab-content>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Dynamic Fields for {{selectedChartType?.name}}</h3>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="showAddDynamicFieldForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Dynamic Field Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddDynamicFieldForm">
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||||
|
<label>Show in UI</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="createDynamicField()" [disabled]="!newDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button class="btn" (click)="showAddDynamicFieldForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic Fields Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Field Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Show in UI</clr-dg-column>
|
||||||
|
<clr-dg-column>Sort Order</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let field of dynamicFields" [clrDgItem]="field">
|
||||||
|
<clr-dg-cell>{{field.fieldName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.fieldLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.fieldType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="field.isRequired" [class.label-danger]="!field.isRequired">
|
||||||
|
{{field.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="field.showInUi" [class.label-danger]="!field.showInUi">
|
||||||
|
{{field.showInUi ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.sortOrder}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{field.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(field)" title="Edit">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(field.id)" title="Delete">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{dynamicFields.length}} field(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Dynamic Field Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedDynamicField">
|
||||||
|
<h4>Edit Dynamic Field</h4>
|
||||||
|
<form clrForm>
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||||
|
<label>Show in UI</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" (click)="updateDynamicField()" [disabled]="!selectedDynamicField.fieldName || dynamicFieldLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="dynamicFieldLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
<button class="btn" (click)="selectedDynamicField = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</clr-tab-content>
|
||||||
|
</clr-tab>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
.chart-config-manager {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-success {
|
||||||
|
background-color: #318700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-danger {
|
||||||
|
background-color: #e62200;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-tab-content {
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-cell {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn-icon {
|
||||||
|
min-width: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure icons are visible
|
||||||
|
cds-icon {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}cds-icon {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,710 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartTypeService } from '../chart-type-manager/chart-type.service';
|
||||||
|
import { UiComponentService } from './ui-component.service';
|
||||||
|
import { ComponentPropertyService } from './component-property.service';
|
||||||
|
import { ChartTemplateService } from './chart-template.service';
|
||||||
|
import { DynamicFieldService } from './dynamic-field.service';
|
||||||
|
|
||||||
|
export interface ChartType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiComponent {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
componentName: string;
|
||||||
|
componentType: string;
|
||||||
|
displayLabel: string;
|
||||||
|
placeholder: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComponentProperty {
|
||||||
|
id: number;
|
||||||
|
component: UiComponent;
|
||||||
|
propertyName: string;
|
||||||
|
propertyValue: string;
|
||||||
|
propertyType: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChartTemplate {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
templateName: string;
|
||||||
|
templateHtml: string;
|
||||||
|
templateCss: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DynamicField {
|
||||||
|
id: number;
|
||||||
|
chartType: ChartType;
|
||||||
|
fieldName: string;
|
||||||
|
fieldLabel: string;
|
||||||
|
fieldType: string;
|
||||||
|
fieldOptions: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
showInUi: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-config-manager',
|
||||||
|
templateUrl: './chart-config-manager.component.html',
|
||||||
|
styleUrls: ['./chart-config-manager.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartConfigManagerComponent implements OnInit {
|
||||||
|
// Chart Types
|
||||||
|
chartTypes: ChartType[] = [];
|
||||||
|
selectedChartType: ChartType | null = null;
|
||||||
|
newChartType: Partial<ChartType> = {};
|
||||||
|
showAddChartTypeForm = false;
|
||||||
|
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// UI Components
|
||||||
|
uiComponents: UiComponent[] = [];
|
||||||
|
selectedUiComponent: UiComponent | null = null;
|
||||||
|
newUiComponent: Partial<UiComponent> = {};
|
||||||
|
showAddUiComponentForm = false;
|
||||||
|
uiComponentLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Component Properties
|
||||||
|
componentProperties: ComponentProperty[] = [];
|
||||||
|
selectedComponentProperty: ComponentProperty | null = null;
|
||||||
|
newComponentProperty: Partial<ComponentProperty> = {};
|
||||||
|
showAddComponentPropertyForm = false;
|
||||||
|
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Chart Templates
|
||||||
|
chartTemplates: ChartTemplate[] = [];
|
||||||
|
selectedChartTemplate: ChartTemplate | null = null;
|
||||||
|
newChartTemplate: Partial<ChartTemplate> = {};
|
||||||
|
showAddChartTemplateForm = false;
|
||||||
|
chartTemplateLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Dynamic Fields
|
||||||
|
dynamicFields: DynamicField[] = [];
|
||||||
|
selectedDynamicField: DynamicField | null = null;
|
||||||
|
newDynamicField: Partial<DynamicField> = {};
|
||||||
|
showAddDynamicFieldForm = false;
|
||||||
|
dynamicFieldLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private uiComponentService: UiComponentService,
|
||||||
|
private componentPropertyService: ComponentPropertyService,
|
||||||
|
private chartTemplateService: ChartTemplateService,
|
||||||
|
private dynamicFieldService: DynamicFieldService
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
console.log('ChartConfigManagerComponent initialized');
|
||||||
|
this.loadChartTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart Type Methods
|
||||||
|
loadChartTypes(): void {
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
console.log('Loading chart types...');
|
||||||
|
this.chartTypeService.getAllChartTypes().subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
console.log('Chart types loaded:', data);
|
||||||
|
this.chartTypes = data;
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart types:', error);
|
||||||
|
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartType(): void {
|
||||||
|
if (!this.newChartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.createChartType(this.newChartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTypes.push(data);
|
||||||
|
this.newChartType = {};
|
||||||
|
this.showAddChartTypeForm = false;
|
||||||
|
this.showSuccess('Chart type created successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart type:', error);
|
||||||
|
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartType(): void {
|
||||||
|
if (!this.selectedChartType || !this.selectedChartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.updateChartType(this.selectedChartType.id, this.selectedChartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.chartTypes.findIndex(ct => ct.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.chartTypes[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.showSuccess('Chart type updated successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart type:', error);
|
||||||
|
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartType(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart type? This will also delete all related UI components, templates, and fields.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.deleteChartType(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||||
|
// Clear related data if the deleted chart type was selected
|
||||||
|
if (this.selectedChartType && this.selectedChartType.id === id) {
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.uiComponents = [];
|
||||||
|
this.chartTemplates = [];
|
||||||
|
this.dynamicFields = [];
|
||||||
|
}
|
||||||
|
this.showSuccess('Chart type deleted successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart type:', error);
|
||||||
|
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChartTypeForEdit(chartType: ChartType): void {
|
||||||
|
this.selectedChartType = { ...chartType };
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Component Methods
|
||||||
|
loadUiComponents(chartTypeId: number): void {
|
||||||
|
if (!chartTypeId) {
|
||||||
|
this.uiComponents = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents = data;
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading UI components:', error);
|
||||||
|
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createUiComponent(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Create a complete chartType object with only the ID
|
||||||
|
const chartTypeWithId = {
|
||||||
|
id: this.selectedChartType.id,
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
description: '',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const uiComponentData = {
|
||||||
|
...this.newUiComponent,
|
||||||
|
chartType: chartTypeWithId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uiComponentService.createUiComponent(uiComponentData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents.push(data);
|
||||||
|
this.newUiComponent = {};
|
||||||
|
this.showAddUiComponentForm = false;
|
||||||
|
this.showSuccess('UI component created successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating UI component:', error);
|
||||||
|
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUiComponent(): void {
|
||||||
|
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.uiComponents[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
this.showSuccess('UI component updated successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating UI component:', error);
|
||||||
|
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUiComponent(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.deleteUiComponent(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||||
|
this.showSuccess('UI component deleted successfully');
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting UI component:', error);
|
||||||
|
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.uiComponentLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||||
|
this.selectedUiComponent = { ...uiComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component Property Methods
|
||||||
|
loadComponentProperties(componentId: number): void {
|
||||||
|
if (!componentId) {
|
||||||
|
this.componentProperties = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties = data;
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading component properties:', error);
|
||||||
|
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createComponentProperty(): void {
|
||||||
|
if (!this.selectedUiComponent) {
|
||||||
|
this.showError('Please select a UI component first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Create a complete component object with only the ID
|
||||||
|
const componentWithId = {
|
||||||
|
id: this.selectedUiComponent.id
|
||||||
|
} as UiComponent;
|
||||||
|
|
||||||
|
const componentPropertyData = {
|
||||||
|
...this.newComponentProperty,
|
||||||
|
component: componentWithId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties.push(data);
|
||||||
|
this.newComponentProperty = {};
|
||||||
|
this.showAddComponentPropertyForm = false;
|
||||||
|
this.showSuccess('Component property created successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating component property:', error);
|
||||||
|
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComponentProperty(): void {
|
||||||
|
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.componentProperties[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
this.showSuccess('Component property updated successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating component property:', error);
|
||||||
|
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteComponentProperty(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.deleteComponentProperty(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||||
|
this.showSuccess('Component property deleted successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting component property:', error);
|
||||||
|
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||||
|
this.selectedComponentProperty = { ...componentProperty };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart Template Methods
|
||||||
|
loadChartTemplates(chartTypeId: number): void {
|
||||||
|
if (!chartTypeId) {
|
||||||
|
this.chartTemplates = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates = data;
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart templates:', error);
|
||||||
|
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartTemplate(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Remove the chartType from the template data since we're passing it as a parameter
|
||||||
|
const chartTemplateData = { ...this.newChartTemplate };
|
||||||
|
// Remove chartType property if it exists
|
||||||
|
delete (chartTemplateData as any).chartType;
|
||||||
|
|
||||||
|
this.chartTemplateService.createChartTemplate(chartTemplateData, this.selectedChartType.id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates.push(data);
|
||||||
|
this.newChartTemplate = {};
|
||||||
|
this.showAddChartTemplateForm = false;
|
||||||
|
this.showSuccess('Chart template created successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart template:', error);
|
||||||
|
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartTemplate(): void {
|
||||||
|
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.chartTemplates[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
this.showSuccess('Chart template updated successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart template:', error);
|
||||||
|
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartTemplate(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.deleteChartTemplate(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||||
|
this.showSuccess('Chart template deleted successfully');
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart template:', error);
|
||||||
|
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTemplateLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||||
|
this.selectedChartTemplate = { ...chartTemplate };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic Field Methods
|
||||||
|
loadDynamicFields(chartTypeId: number): void {
|
||||||
|
if (!chartTypeId) {
|
||||||
|
this.dynamicFields = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields = data;
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading dynamic fields:', error);
|
||||||
|
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createDynamicField(): void {
|
||||||
|
if (!this.selectedChartType) {
|
||||||
|
this.showError('Please select a chart type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Remove the chartType from the dynamic field data since we're passing it as a parameter
|
||||||
|
const dynamicFieldData = { ...this.newDynamicField };
|
||||||
|
// Remove chartType property if it exists
|
||||||
|
delete (dynamicFieldData as any).chartType;
|
||||||
|
|
||||||
|
this.dynamicFieldService.createDynamicField(dynamicFieldData, this.selectedChartType.id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields.push(data);
|
||||||
|
this.newDynamicField = {};
|
||||||
|
this.showAddDynamicFieldForm = false;
|
||||||
|
this.showSuccess('Dynamic field created successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating dynamic field:', error);
|
||||||
|
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDynamicField(): void {
|
||||||
|
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.dynamicFields[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
this.showSuccess('Dynamic field updated successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating dynamic field:', error);
|
||||||
|
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDynamicField(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.deleteDynamicField(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||||
|
this.showSuccess('Dynamic field deleted successfully');
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting dynamic field:', error);
|
||||||
|
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.dynamicFieldLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||||
|
this.selectedDynamicField = { ...dynamicField };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
onChartTypeSelect(chartType: ChartType): void {
|
||||||
|
this.selectedChartType = chartType;
|
||||||
|
this.loadUiComponents(chartType.id);
|
||||||
|
this.loadChartTemplates(chartType.id);
|
||||||
|
this.loadDynamicFields(chartType.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUiComponentSelect(uiComponent: UiComponent): void {
|
||||||
|
this.selectedUiComponent = uiComponent;
|
||||||
|
this.loadComponentProperties(uiComponent.id);
|
||||||
|
// Scroll to the Component Properties tab
|
||||||
|
setTimeout(() => {
|
||||||
|
const element = document.querySelector('clr-tab[ng-reflect-ng-if="true"] button[clr-tab-link]:nth-child(3)');
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetForms(): void {
|
||||||
|
this.newChartType = {};
|
||||||
|
this.newUiComponent = {};
|
||||||
|
this.newComponentProperty = {};
|
||||||
|
this.newChartTemplate = {};
|
||||||
|
this.newDynamicField = {};
|
||||||
|
this.showAddChartTypeForm = false;
|
||||||
|
this.showAddUiComponentForm = false;
|
||||||
|
this.showAddComponentPropertyForm = false;
|
||||||
|
this.showAddChartTemplateForm = false;
|
||||||
|
this.showAddDynamicFieldForm = false;
|
||||||
|
this.selectedChartType = null;
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { ChartTemplate } from './chart-config-manager.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ChartTemplateService {
|
||||||
|
private chartTemplatesUrl = 'api/chart-templates';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
// Get all chart templates for a chart type
|
||||||
|
getChartTemplatesByChartType(chartTypeId: number): Observable<ChartTemplate[]> {
|
||||||
|
const url = `${this.chartTemplatesUrl}/chart-type/${chartTypeId}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chart template by ID
|
||||||
|
getChartTemplateById(id: number): Observable<ChartTemplate> {
|
||||||
|
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new chart template with optional chart type ID as parameter
|
||||||
|
createChartTemplate(chartTemplate: Partial<ChartTemplate>, chartTypeId?: number): Observable<ChartTemplate> {
|
||||||
|
let url = this.chartTemplatesUrl;
|
||||||
|
if (chartTypeId) {
|
||||||
|
url = `${this.chartTemplatesUrl}?chartTypeId=${chartTypeId}`;
|
||||||
|
}
|
||||||
|
return this.apiRequest.post(url, chartTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart template
|
||||||
|
updateChartTemplate(id: number, chartTemplate: ChartTemplate): Observable<ChartTemplate> {
|
||||||
|
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||||
|
return this.apiRequest.put(url, chartTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete chart template
|
||||||
|
deleteChartTemplate(id: number): Observable<void> {
|
||||||
|
const url = `${this.chartTemplatesUrl}/${id}`;
|
||||||
|
return this.apiRequest.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { ComponentProperty } from './chart-config-manager.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ComponentPropertyService {
|
||||||
|
private componentPropertiesUrl = 'api/component-properties';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
// Get all component properties for a component
|
||||||
|
getComponentPropertiesByComponent(componentId: number): Observable<ComponentProperty[]> {
|
||||||
|
const url = `${this.componentPropertiesUrl}/component/${componentId}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get component property by ID
|
||||||
|
getComponentPropertyById(id: number): Observable<ComponentProperty> {
|
||||||
|
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new component property
|
||||||
|
createComponentProperty(componentProperty: Partial<ComponentProperty>): Observable<ComponentProperty> {
|
||||||
|
return this.apiRequest.post(this.componentPropertiesUrl, componentProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update component property
|
||||||
|
updateComponentProperty(id: number, componentProperty: ComponentProperty): Observable<ComponentProperty> {
|
||||||
|
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||||
|
return this.apiRequest.put(url, componentProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete component property
|
||||||
|
deleteComponentProperty(id: number): Observable<void> {
|
||||||
|
const url = `${this.componentPropertiesUrl}/${id}`;
|
||||||
|
return this.apiRequest.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable, forkJoin } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { ChartType, UiComponent, ComponentProperty, ChartTemplate, DynamicField } from './chart-config-manager.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DynamicChartLoaderService {
|
||||||
|
private chartTypesUrl = 'api/chart-types';
|
||||||
|
private uiComponentsUrl = 'api/ui-components';
|
||||||
|
private componentPropertiesUrl = 'api/component-properties';
|
||||||
|
private chartTemplatesUrl = 'api/chart-templates';
|
||||||
|
private dynamicFieldsUrl = 'api/dynamic-fields';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all chart configurations dynamically
|
||||||
|
* This method fetches all chart types and their associated components, templates, and fields
|
||||||
|
*/
|
||||||
|
loadAllChartConfigurations(): Observable<any> {
|
||||||
|
console.log('Loading all chart configurations dynamically');
|
||||||
|
|
||||||
|
// Load all chart types first
|
||||||
|
return this.apiRequest.get(this.chartTypesUrl).pipe(
|
||||||
|
map(chartTypes => {
|
||||||
|
console.log('Loaded chart types:', chartTypes);
|
||||||
|
return chartTypes;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load complete configuration for a specific chart type
|
||||||
|
* This includes UI components, templates, and dynamic fields
|
||||||
|
*/
|
||||||
|
loadChartConfiguration(chartTypeId: number): Observable<{
|
||||||
|
chartType: ChartType,
|
||||||
|
uiComponents: UiComponent[],
|
||||||
|
templates: ChartTemplate[],
|
||||||
|
dynamicFields: DynamicField[]
|
||||||
|
}> {
|
||||||
|
console.log(`Loading complete configuration for chart type ${chartTypeId}`);
|
||||||
|
|
||||||
|
// Load all related data in parallel
|
||||||
|
return forkJoin({
|
||||||
|
chartType: this.apiRequest.get(`${this.chartTypesUrl}/${chartTypeId}`),
|
||||||
|
uiComponents: this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`),
|
||||||
|
templates: this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`),
|
||||||
|
dynamicFields: this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`)
|
||||||
|
}).pipe(
|
||||||
|
map(result => {
|
||||||
|
console.log(`Loaded complete configuration for chart type ${chartTypeId}:`, result);
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load chart template for a specific chart type
|
||||||
|
* This is used to render the chart UI dynamically
|
||||||
|
*/
|
||||||
|
loadChartTemplate(chartTypeId: number): Observable<ChartTemplate[]> {
|
||||||
|
console.log(`Loading chart templates for chart type ${chartTypeId}`);
|
||||||
|
return this.apiRequest.get(`${this.chartTemplatesUrl}/chart-type/${chartTypeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load UI components for a specific chart type
|
||||||
|
* These define what configuration fields are needed for the chart
|
||||||
|
*/
|
||||||
|
loadUiComponents(chartTypeId: number): Observable<UiComponent[]> {
|
||||||
|
console.log(`Loading UI components for chart type ${chartTypeId}`);
|
||||||
|
return this.apiRequest.get(`${this.uiComponentsUrl}/chart-type/${chartTypeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load dynamic fields for a specific chart type
|
||||||
|
* These define additional dynamic fields that can be used in the chart
|
||||||
|
*/
|
||||||
|
loadDynamicFields(chartTypeId: number): Observable<DynamicField[]> {
|
||||||
|
console.log(`Loading dynamic fields for chart type ${chartTypeId}`);
|
||||||
|
return this.apiRequest.get(`${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chart type by name
|
||||||
|
* This is useful for finding a chart type by its name rather than ID
|
||||||
|
*/
|
||||||
|
// getChartTypeByName(name: string): Observable<ChartType | null> {
|
||||||
|
// console.log(`Finding chart type by name: ${name}`);
|
||||||
|
// return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`).pipe(
|
||||||
|
// map((chartTypes: ChartType[]) => {
|
||||||
|
// console.log('Available chart types:', chartTypes);
|
||||||
|
// const chartType = chartTypes.find(ct => ct.name === name);
|
||||||
|
// console.log(`Found chart type for name ${name}:`, chartType);
|
||||||
|
// return chartType || null;
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
getChartTypeByName(name: string): Observable<any> {
|
||||||
|
console.log(`Finding chart type by name: ${name}`);
|
||||||
|
return this.apiRequest.get(`${this.chartTypesUrl}/byname?chartName=${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all active chart types
|
||||||
|
* This is used to populate the chart selection in the dashboard editor
|
||||||
|
*/
|
||||||
|
loadActiveChartTypes(): Observable<ChartType[]> {
|
||||||
|
console.log('Loading active chart types');
|
||||||
|
return this.apiRequest.get(`${this.chartTypesUrl}`).pipe(
|
||||||
|
map((chartTypes: ChartType[]) => {
|
||||||
|
const activeChartTypes = chartTypes.filter(ct => ct.isActive);
|
||||||
|
console.log('Loaded active chart types:', activeChartTypes);
|
||||||
|
return activeChartTypes;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { DynamicField } from './chart-config-manager.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class DynamicFieldService {
|
||||||
|
private dynamicFieldsUrl = 'api/dynamic-fields';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
// Get all dynamic fields for a chart type
|
||||||
|
getDynamicFieldsByChartType(chartTypeId: number): Observable<DynamicField[]> {
|
||||||
|
const url = `${this.dynamicFieldsUrl}/chart-type/${chartTypeId}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dynamic field by ID
|
||||||
|
getDynamicFieldById(id: number): Observable<DynamicField> {
|
||||||
|
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new dynamic field with optional chart type ID as parameter
|
||||||
|
createDynamicField(dynamicField: Partial<DynamicField>, chartTypeId?: number): Observable<DynamicField> {
|
||||||
|
let url = this.dynamicFieldsUrl;
|
||||||
|
if (chartTypeId) {
|
||||||
|
url = `${this.dynamicFieldsUrl}?chartTypeId=${chartTypeId}`;
|
||||||
|
}
|
||||||
|
return this.apiRequest.post(url, dynamicField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dynamic field
|
||||||
|
updateDynamicField(id: number, dynamicField: DynamicField): Observable<DynamicField> {
|
||||||
|
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||||
|
return this.apiRequest.put(url, dynamicField);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete dynamic field
|
||||||
|
deleteDynamicField(id: number): Observable<void> {
|
||||||
|
const url = `${this.dynamicFieldsUrl}/${id}`;
|
||||||
|
return this.apiRequest.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="chart-template-form">
|
||||||
|
<form clrForm (ngSubmit)="onSubmit()" #chartTemplateForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Template Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartTemplate.templateName" name="templateName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>HTML Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="chartTemplate.templateHtml" name="templateHtml" rows="5" placeholder="<div class='chart-container'>...</div>"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>CSS Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="chartTemplate.templateCss" name="templateCss" rows="5" placeholder=".chart-container { ... }"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Default</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="chartTemplate.isDefault" name="isDefault" />
|
||||||
|
<label>Default Template</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!chartTemplate.templateName">
|
||||||
|
{{ isEdit ? 'Update' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.chart-template-form {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { ChartTemplate } from '../chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-template-form',
|
||||||
|
templateUrl: './chart-template-form.component.html',
|
||||||
|
styleUrls: ['./chart-template-form.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTemplateFormComponent {
|
||||||
|
@Input() chartTemplate: Partial<ChartTemplate> = {};
|
||||||
|
@Input() isEdit = false;
|
||||||
|
@Output() save = new EventEmitter<Partial<ChartTemplate>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.save.emit(this.chartTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<div class="chart-type-form">
|
||||||
|
<form clrForm (ngSubmit)="onSubmit()" #chartTypeForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Active</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||||
|
<label>Active</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name">
|
||||||
|
{{ isEdit ? 'Update' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.chart-type-form {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { ChartType } from '../chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-form',
|
||||||
|
templateUrl: './chart-type-form.component.html',
|
||||||
|
styleUrls: ['./chart-type-form.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTypeFormComponent {
|
||||||
|
@Input() chartType: Partial<ChartType> = {};
|
||||||
|
@Input() isEdit = false;
|
||||||
|
@Output() save = new EventEmitter<Partial<ChartType>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.save.emit(this.chartType);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="component-property-form">
|
||||||
|
<form clrForm (ngSubmit)="onSubmit()" #componentPropertyForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="componentProperty.propertyName" name="propertyName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Value</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="componentProperty.propertyValue" name="propertyValue" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-select-container>
|
||||||
|
<label>Property Type</label>
|
||||||
|
<select clrSelect [(ngModel)]="componentProperty.propertyType" name="propertyType">
|
||||||
|
<option value="">Select a type</option>
|
||||||
|
<option *ngFor="let type of propertyTypes" [value]="type">{{ type | titlecase }}</option>
|
||||||
|
</select>
|
||||||
|
</clr-select-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!componentProperty.propertyName">
|
||||||
|
{{ isEdit ? 'Update' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.component-property-form {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { ComponentProperty } from '../chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-component-property-form',
|
||||||
|
templateUrl: './component-property-form.component.html',
|
||||||
|
styleUrls: ['./component-property-form.component.scss']
|
||||||
|
})
|
||||||
|
export class ComponentPropertyFormComponent {
|
||||||
|
@Input() componentProperty: Partial<ComponentProperty> = {};
|
||||||
|
@Input() isEdit = false;
|
||||||
|
@Output() save = new EventEmitter<Partial<ComponentProperty>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
propertyTypes = [
|
||||||
|
'string',
|
||||||
|
'number',
|
||||||
|
'boolean',
|
||||||
|
'array',
|
||||||
|
'object',
|
||||||
|
'function'
|
||||||
|
];
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.save.emit(this.componentProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<div class="dynamic-field-form">
|
||||||
|
<form clrForm (ngSubmit)="onSubmit()" #dynamicFieldForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="dynamicField.fieldName" name="fieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="dynamicField.fieldLabel" name="fieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-select-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<select clrSelect [(ngModel)]="dynamicField.fieldType" name="fieldType">
|
||||||
|
<option value="">Select a type</option>
|
||||||
|
<option *ngFor="let type of fieldTypes" [value]="type">{{ type | titlecase }}</option>
|
||||||
|
</select>
|
||||||
|
</clr-select-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options (JSON format)</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="dynamicField.fieldOptions" name="fieldOptions" rows="3" placeholder='{"option1": "Option 1", "option2": "Option 2"}'></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="dynamicField.sortOrder" name="fieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.isRequired" name="fieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="dynamicField.showInUi" name="fieldShowInUi" />
|
||||||
|
<label>Show in UI</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!dynamicField.fieldName">
|
||||||
|
{{ isEdit ? 'Update' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.dynamic-field-form {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { DynamicField } from '../chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-dynamic-field-form',
|
||||||
|
templateUrl: './dynamic-field-form.component.html',
|
||||||
|
styleUrls: ['./dynamic-field-form.component.scss']
|
||||||
|
})
|
||||||
|
export class DynamicFieldFormComponent {
|
||||||
|
@Input() dynamicField: Partial<DynamicField> = {};
|
||||||
|
@Input() isEdit = false;
|
||||||
|
@Output() save = new EventEmitter<Partial<DynamicField>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
fieldTypes = [
|
||||||
|
'text',
|
||||||
|
'number',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'date',
|
||||||
|
'select',
|
||||||
|
'checkbox',
|
||||||
|
'radio',
|
||||||
|
'textarea'
|
||||||
|
];
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.save.emit(this.dynamicField);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="ui-component-form">
|
||||||
|
<form clrForm (ngSubmit)="onSubmit()" #uiComponentForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="uiComponent.componentName" name="componentName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-select-container>
|
||||||
|
<label>Component Type</label>
|
||||||
|
<select clrSelect [(ngModel)]="uiComponent.componentType" name="componentType">
|
||||||
|
<option value="">Select a type</option>
|
||||||
|
<option *ngFor="let type of componentTypes" [value]="type">{{ type | titlecase }}</option>
|
||||||
|
</select>
|
||||||
|
</clr-select-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="uiComponent.displayLabel" name="displayLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Placeholder</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="uiComponent.placeholder" name="placeholder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="uiComponent.sortOrder" name="sortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="uiComponent.isRequired" name="isRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!uiComponent.componentName">
|
||||||
|
{{ isEdit ? 'Update' : 'Save' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.ui-component-form {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||||
|
import { UiComponent } from '../chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-ui-component-form',
|
||||||
|
templateUrl: './ui-component-form.component.html',
|
||||||
|
styleUrls: ['./ui-component-form.component.scss']
|
||||||
|
})
|
||||||
|
export class UiComponentFormComponent {
|
||||||
|
@Input() uiComponent: Partial<UiComponent> = {};
|
||||||
|
@Input() isEdit = false;
|
||||||
|
@Output() save = new EventEmitter<Partial<UiComponent>>();
|
||||||
|
@Output() cancel = new EventEmitter<void>();
|
||||||
|
|
||||||
|
componentTypes = [
|
||||||
|
'input',
|
||||||
|
'select',
|
||||||
|
'checkbox',
|
||||||
|
'radio',
|
||||||
|
'textarea',
|
||||||
|
'datepicker',
|
||||||
|
'slider',
|
||||||
|
'toggle'
|
||||||
|
];
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.save.emit(this.uiComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
import { UiComponent } from './chart-config-manager.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class UiComponentService {
|
||||||
|
private uiComponentsUrl = 'api/ui-components';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
// Get all UI components for a chart type
|
||||||
|
getUiComponentsByChartType(chartTypeId: number): Observable<UiComponent[]> {
|
||||||
|
const url = `${this.uiComponentsUrl}/chart-type/${chartTypeId}`;
|
||||||
|
console.log(`Fetching UI components for chart type ${chartTypeId} from ${url}`);
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get UI component by ID
|
||||||
|
getUiComponentById(id: number): Observable<UiComponent> {
|
||||||
|
const url = `${this.uiComponentsUrl}/${id}`;
|
||||||
|
console.log(`Fetching UI component ${id} from ${url}`);
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new UI component
|
||||||
|
createUiComponent(uiComponent: Partial<UiComponent>): Observable<UiComponent> {
|
||||||
|
console.log('Creating UI component:', uiComponent);
|
||||||
|
return this.apiRequest.post(this.uiComponentsUrl, uiComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI component
|
||||||
|
updateUiComponent(id: number, uiComponent: UiComponent): Observable<UiComponent> {
|
||||||
|
const url = `${this.uiComponentsUrl}/${id}`;
|
||||||
|
console.log(`Updating UI component ${id}:`, uiComponent);
|
||||||
|
return this.apiRequest.put(url, uiComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete UI component
|
||||||
|
deleteUiComponent(id: number): Observable<void> {
|
||||||
|
const url = `${this.uiComponentsUrl}/${id}`;
|
||||||
|
console.log(`Deleting UI component ${id}`);
|
||||||
|
return this.apiRequest.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="add-chart-type-page">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Add New Chart Type</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="createChartType()" #addChartTypeForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||||
|
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||||
|
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||||
|
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Active</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||||
|
<label>Active</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Create Chart Type
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h4>About Chart Types</h4>
|
||||||
|
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Associated UI components that define the configuration form</li>
|
||||||
|
<li>Templates that define how the chart is rendered</li>
|
||||||
|
<li>Dynamic fields that capture specific configuration parameters</li>
|
||||||
|
</ul>
|
||||||
|
<p>After creating a chart type, you can configure its components, templates, and fields in the chart configuration manager.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
.add-chart-type-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-left: 4px solid #0079b8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-add-chart-type',
|
||||||
|
templateUrl: './add-chart-type.component.html',
|
||||||
|
styleUrls: ['./add-chart-type.component.scss']
|
||||||
|
})
|
||||||
|
export class AddChartTypeComponent {
|
||||||
|
chartType: Partial<ChartType> = {
|
||||||
|
isActive: true
|
||||||
|
};
|
||||||
|
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartType(): void {
|
||||||
|
if (!this.chartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.createChartType(this.chartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.showSuccess('Chart type created successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
// Redirect to chart types list after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart type:', error);
|
||||||
|
this.showError('Error creating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<div class="chart-type-fields-page">
|
||||||
|
<div class="header">
|
||||||
|
<h2>
|
||||||
|
<button class="btn btn-link back-button" (click)="goBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
</button>
|
||||||
|
Dynamic Fields for {{ chartType?.name || 'Chart Type' }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Dynamic Field Form -->
|
||||||
|
<div class="card" *ngIf="showAddForm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Add New Dynamic Field</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="createDynamicField()" #addDynamicFieldForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldName" name="fieldName" required />
|
||||||
|
<clr-control-helper>Enter a unique name for the dynamic field (e.g., "chart-title", "x-axis-label")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldLabel" name="fieldLabel" />
|
||||||
|
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newDynamicField.fieldType" name="fieldType" />
|
||||||
|
<clr-control-helper>Type of the field (e.g., "string", "number", "boolean")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newDynamicField.fieldOptions" name="fieldOptions" rows="3"></textarea>
|
||||||
|
<clr-control-helper>JSON options for the field (for select fields, etc.)</clr-control-helper>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="newDynamicField.sortOrder" name="fieldSortOrder" />
|
||||||
|
<clr-control-helper>Order in which fields appear in the form</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.isRequired" name="fieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Mark as required if this field must be filled</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newDynamicField.showInUi" name="fieldShowInUi" />
|
||||||
|
<label>Visible</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Uncheck to hide this field in the configuration form</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!newDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Create Dynamic Field
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic Fields Grid -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="clr-row">
|
||||||
|
<div class="clr-col">
|
||||||
|
<h3>Dynamic Fields</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col" style="text-align: right;">
|
||||||
|
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Dynamic Field
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Field Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Field Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Visible</clr-dg-column>
|
||||||
|
<clr-dg-column>Sort Order</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let dynamicField of dynamicFields" [clrDgItem]="dynamicField">
|
||||||
|
<clr-dg-cell>{{dynamicField.fieldName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{dynamicField.fieldLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{dynamicField.fieldType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="dynamicField.isRequired" [class.label-danger]="!dynamicField.isRequired">
|
||||||
|
{{dynamicField.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="dynamicField.showInUi" [class.label-danger]="!dynamicField.showInUi">
|
||||||
|
{{dynamicField.showInUi ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{dynamicField.sortOrder}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectDynamicFieldForEdit(dynamicField)" title="Edit">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteDynamicField(dynamicField.id)" title="Delete">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{dynamicFields.length}} dynamic field(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Dynamic Field Form -->
|
||||||
|
<div class="card" *ngIf="selectedDynamicField">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Edit Dynamic Field</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="updateDynamicField()" #editDynamicFieldForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldName" name="editFieldName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldLabel" name="editFieldLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Field Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedDynamicField.fieldType" name="editFieldType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Field Options</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedDynamicField.fieldOptions" name="editFieldOptions" rows="3"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="selectedDynamicField.sortOrder" name="editFieldSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.isRequired" name="editFieldIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Show in UI</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedDynamicField.showInUi" name="editFieldShowInUi" />
|
||||||
|
<label>Visible</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!selectedDynamicField.fieldName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update Dynamic Field
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="selectedDynamicField = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h4>About Dynamic Fields</h4>
|
||||||
|
<p>Dynamic fields define the configurable parameters for a chart type. Each field represents:</p>
|
||||||
|
<ul>
|
||||||
|
<li>A configuration option that can be set when creating or editing a chart</li>
|
||||||
|
<li>Metadata like type, label, and validation rules</li>
|
||||||
|
<li>Visibility and requirement settings</li>
|
||||||
|
</ul>
|
||||||
|
<p>Dynamic fields allow you to create flexible chart configurations that can be customized per instance.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
.chart-type-fields-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 0;
|
||||||
|
cds-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.label-success {
|
||||||
|
background-color: #3d9970;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-danger {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
clr-dg-cell {
|
||||||
|
&:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-left: 4px solid #0079b8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
.clr-row {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.clr-col {
|
||||||
|
text-align: left !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
import { DynamicField } from '../chart-config/chart-config-manager.component';
|
||||||
|
import { DynamicFieldService } from '../chart-config/dynamic-field.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-fields',
|
||||||
|
templateUrl: './chart-type-fields.component.html',
|
||||||
|
styleUrls: ['./chart-type-fields.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTypeFieldsComponent implements OnInit {
|
||||||
|
chartType: ChartType | null = null;
|
||||||
|
dynamicFields: DynamicField[] = [];
|
||||||
|
newDynamicField: Partial<DynamicField> = {
|
||||||
|
isRequired: false,
|
||||||
|
showInUi: true
|
||||||
|
};
|
||||||
|
selectedDynamicField: DynamicField | null = null;
|
||||||
|
showAddForm = false;
|
||||||
|
|
||||||
|
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private dynamicFieldService: DynamicFieldService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
|
if (chartTypeId) {
|
||||||
|
this.loadChartType(chartTypeId);
|
||||||
|
this.loadDynamicFields(chartTypeId);
|
||||||
|
} else {
|
||||||
|
this.showError('Invalid chart type ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartType(id: number): void {
|
||||||
|
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartType = data;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart type:', error);
|
||||||
|
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDynamicFields(chartTypeId: number): void {
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.getDynamicFieldsByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields = data;
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading dynamic fields:', error);
|
||||||
|
this.showError('Error loading dynamic fields: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createDynamicField(): void {
|
||||||
|
if (!this.chartType || !this.newDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy without the chartType property
|
||||||
|
const fieldData: Partial<DynamicField> = {
|
||||||
|
fieldName: this.newDynamicField.fieldName,
|
||||||
|
fieldLabel: this.newDynamicField.fieldLabel,
|
||||||
|
fieldType: this.newDynamicField.fieldType,
|
||||||
|
fieldOptions: this.newDynamicField.fieldOptions,
|
||||||
|
isRequired: this.newDynamicField.isRequired,
|
||||||
|
showInUi: this.newDynamicField.showInUi,
|
||||||
|
sortOrder: this.newDynamicField.sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.createDynamicField(fieldData, this.chartType.id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.dynamicFields.push(data);
|
||||||
|
this.newDynamicField = { isRequired: false, showInUi: true };
|
||||||
|
this.showAddForm = false;
|
||||||
|
this.showSuccess('Dynamic field created successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating dynamic field:', error);
|
||||||
|
this.showError('Error creating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDynamicField(): void {
|
||||||
|
if (!this.selectedDynamicField || !this.selectedDynamicField.fieldName) {
|
||||||
|
this.showError('Field name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Updating dynamic field:', this.selectedDynamicField);
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.updateDynamicField(this.selectedDynamicField.id, this.selectedDynamicField).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.dynamicFields.findIndex(df => df.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.dynamicFields[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedDynamicField = null;
|
||||||
|
this.showSuccess('Dynamic field updated successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating dynamic field:', error);
|
||||||
|
this.showError('Error updating dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDynamicField(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this dynamic field?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.dynamicFieldService.deleteDynamicField(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.dynamicFields = this.dynamicFields.filter(df => df.id !== id);
|
||||||
|
this.showSuccess('Dynamic field deleted successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting dynamic field:', error);
|
||||||
|
this.showError('Error deleting dynamic field: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDynamicFieldForEdit(dynamicField: DynamicField): void {
|
||||||
|
this.selectedDynamicField = { ...dynamicField };
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack(): void {
|
||||||
|
if (this.chartType) {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<div class="chart-type-manager">
|
||||||
|
<h2>Chart Type Management</h2>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Types Grid -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="clr-row">
|
||||||
|
<div class="clr-col">
|
||||||
|
<h3>Chart Types</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col" style="text-align: right;">
|
||||||
|
<button class="btn btn-primary" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/add']">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Chart Type
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="chartTypeLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Description</clr-dg-column>
|
||||||
|
<clr-dg-column>Status</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let chartType of chartTypes" [clrDgItem]="chartType">
|
||||||
|
<clr-dg-cell>{{chartType.name}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.displayName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.description}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||||
|
{{chartType.isActive ? 'Active' : 'Inactive'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.createdAt ? (chartType.createdAt | date:'short') : 'N/A'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartType.updatedAt ? (chartType.updatedAt | date:'short') : 'N/A'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" [routerLink]="['/cns-portal/dashboardbuilder/chart-types/edit', chartType.id]" title="Edit">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartType(chartType.id)" title="Delete">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTypes.length}} chart type(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
.chart-type-manager {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.label-success {
|
||||||
|
background-color: #3d9970;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-danger {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
clr-dg-cell {
|
||||||
|
&:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
.clr-row {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.clr-col {
|
||||||
|
text-align: left !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-manager',
|
||||||
|
templateUrl: './chart-type-manager.component.html',
|
||||||
|
styleUrls: ['./chart-type-manager.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTypeManagerComponent implements OnInit {
|
||||||
|
chartTypes: ChartType[] = [];
|
||||||
|
chartTypeLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
constructor(private chartTypeService: ChartTypeService) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadChartTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chart Type Methods
|
||||||
|
loadChartTypes(): void {
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.getAllChartTypes().subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
// Process the data to ensure dates are properly formatted
|
||||||
|
this.chartTypes = data.map(chartType => ({
|
||||||
|
...chartType,
|
||||||
|
createdAt: this.formatDate(chartType.createdAt),
|
||||||
|
updatedAt: this.formatDate(chartType.updatedAt)
|
||||||
|
}));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart types:', error);
|
||||||
|
this.showError('Error loading chart types: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date to handle both string and object formats
|
||||||
|
private formatDate(date: any): string {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
// If it's already a string, return as is
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an object, try to convert to string
|
||||||
|
if (typeof date === 'object') {
|
||||||
|
// Handle various date object formats
|
||||||
|
if (date instanceof Date) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ISO string within object
|
||||||
|
if (date.date) {
|
||||||
|
return date.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other object formats
|
||||||
|
try {
|
||||||
|
return new Date(date).toISOString();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartType(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart type?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.deleteChartType(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTypes = this.chartTypes.filter(ct => ct.id !== id);
|
||||||
|
this.showSuccess('Chart type deleted successfully');
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart type:', error);
|
||||||
|
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.chartTypeLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-page',
|
||||||
|
template: `
|
||||||
|
<div class="chart-type-page">
|
||||||
|
<app-chart-type-manager></app-chart-type-manager>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
.chart-type-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-type-page {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class ChartTypePageComponent { }
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<div class="chart-type-templates-page">
|
||||||
|
<div class="header">
|
||||||
|
<h2>
|
||||||
|
<button class="btn btn-link back-button" (click)="goBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
</button>
|
||||||
|
Chart Templates for {{ chartType?.name || 'Chart Type' }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Chart Template Form -->
|
||||||
|
<div class="card" *ngIf="showAddForm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Add New Chart Template</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="createChartTemplate()" #addChartTemplateForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Template Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newChartTemplate.templateName" name="templateName" required />
|
||||||
|
<clr-control-helper>Enter a unique name for the chart template</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>HTML Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateHtml" name="templateHtml" rows="5"></textarea>
|
||||||
|
<clr-control-helper>HTML structure for rendering the chart</clr-control-helper>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>CSS Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="newChartTemplate.templateCss" name="templateCss" rows="5"></textarea>
|
||||||
|
<clr-control-helper>CSS styling for the chart template</clr-control-helper>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Default</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newChartTemplate.isDefault" name="isDefault" />
|
||||||
|
<label>Default Template</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Mark as default template for this chart type</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!newChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Create Chart Template
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Templates Grid -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="clr-row">
|
||||||
|
<div class="clr-col">
|
||||||
|
<h3>Chart Templates</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col" style="text-align: right;">
|
||||||
|
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Chart Template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Template Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Default</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let chartTemplate of chartTemplates" [clrDgItem]="chartTemplate">
|
||||||
|
<clr-dg-cell>{{chartTemplate.templateName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="chartTemplate.isDefault" [class.label-danger]="!chartTemplate.isDefault">
|
||||||
|
{{chartTemplate.isDefault ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartTemplate.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{chartTemplate.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectChartTemplateForEdit(chartTemplate)" title="Edit">
|
||||||
|
<cds-icon shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteChartTemplate(chartTemplate.id)" title="Delete">
|
||||||
|
<cds-icon shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{chartTemplates.length}} chart template(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Chart Template Form -->
|
||||||
|
<div class="card" *ngIf="selectedChartTemplate">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Edit Chart Template</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="updateChartTemplate()" #editChartTemplateForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Template Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedChartTemplate.templateName" name="editTemplateName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>HTML Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateHtml" name="editTemplateHtml" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>CSS Template</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="selectedChartTemplate.templateCss" name="editTemplateCss" rows="5"></textarea>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Default</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedChartTemplate.isDefault" name="editIsDefault" />
|
||||||
|
<label>Default Template</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!selectedChartTemplate.templateName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update Chart Template
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="selectedChartTemplate = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h4>About Chart Templates</h4>
|
||||||
|
<p>Chart templates define how a chart of this type is rendered. Each template includes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>HTML structure that defines the chart layout</li>
|
||||||
|
<li>CSS styling that controls the appearance</li>
|
||||||
|
<li>A default flag to indicate the primary template</li>
|
||||||
|
</ul>
|
||||||
|
<p>Templates allow you to have multiple visual representations for the same chart type.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
.chart-type-templates-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 0;
|
||||||
|
cds-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.label-success {
|
||||||
|
background-color: #3d9970;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-danger {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
clr-dg-cell {
|
||||||
|
&:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-left: 4px solid #0079b8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
.clr-row {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.clr-col {
|
||||||
|
text-align: left !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
import { ChartTemplateService } from '../chart-config/chart-template.service';
|
||||||
|
import { ChartTemplate } from '../chart-config/chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-templates',
|
||||||
|
templateUrl: './chart-type-templates.component.html',
|
||||||
|
styleUrls: ['./chart-type-templates.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTypeTemplatesComponent implements OnInit {
|
||||||
|
chartType: ChartType | null = null;
|
||||||
|
chartTemplates: ChartTemplate[] = [];
|
||||||
|
newChartTemplate: Partial<ChartTemplate> = {
|
||||||
|
isDefault: false
|
||||||
|
};
|
||||||
|
selectedChartTemplate: ChartTemplate | null = null;
|
||||||
|
showAddForm = false;
|
||||||
|
|
||||||
|
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private chartTemplateService: ChartTemplateService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
|
if (chartTypeId) {
|
||||||
|
this.loadChartType(chartTypeId);
|
||||||
|
this.loadChartTemplates(chartTypeId);
|
||||||
|
} else {
|
||||||
|
this.showError('Invalid chart type ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartType(id: number): void {
|
||||||
|
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartType = data;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart type:', error);
|
||||||
|
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartTemplates(chartTypeId: number): void {
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.getChartTemplatesByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates = data;
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart templates:', error);
|
||||||
|
this.showError('Error loading chart templates: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createChartTemplate(): void {
|
||||||
|
if (!this.chartType || !this.newChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy without the chartType property
|
||||||
|
const templateData: Partial<ChartTemplate> = {
|
||||||
|
templateName: this.newChartTemplate.templateName,
|
||||||
|
templateHtml: this.newChartTemplate.templateHtml,
|
||||||
|
templateCss: this.newChartTemplate.templateCss,
|
||||||
|
isDefault: this.newChartTemplate.isDefault
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.createChartTemplate(templateData, this.chartType.id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartTemplates.push(data);
|
||||||
|
this.newChartTemplate = { isDefault: false };
|
||||||
|
this.showAddForm = false;
|
||||||
|
this.showSuccess('Chart template created successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating chart template:', error);
|
||||||
|
this.showError('Error creating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartTemplate(): void {
|
||||||
|
if (!this.selectedChartTemplate || !this.selectedChartTemplate.templateName) {
|
||||||
|
this.showError('Template name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.updateChartTemplate(this.selectedChartTemplate.id, this.selectedChartTemplate).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.chartTemplates.findIndex(ct => ct.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.chartTemplates[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedChartTemplate = null;
|
||||||
|
this.showSuccess('Chart template updated successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart template:', error);
|
||||||
|
this.showError('Error updating chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChartTemplate(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this chart template?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTemplateService.deleteChartTemplate(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.chartTemplates = this.chartTemplates.filter(ct => ct.id !== id);
|
||||||
|
this.showSuccess('Chart template deleted successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart template:', error);
|
||||||
|
this.showError('Error deleting chart template: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectChartTemplateForEdit(chartTemplate: ChartTemplate): void {
|
||||||
|
this.selectedChartTemplate = { ...chartTemplate };
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack(): void {
|
||||||
|
if (this.chartType) {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
<div class="chart-type-ui-components-page">
|
||||||
|
<div class="header">
|
||||||
|
<h2>
|
||||||
|
<button class="btn btn-link back-button" (click)="goBack()">
|
||||||
|
<cds-icon shape="arrow" direction="left"></cds-icon>
|
||||||
|
</button>
|
||||||
|
UI Components for {{ chartType?.name || 'Chart Type' }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add UI Component Form -->
|
||||||
|
<div class="card" *ngIf="showAddForm">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Add New UI Component</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="createUiComponent()" #addUiComponentForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.componentName" name="componentName" required />
|
||||||
|
<clr-control-helper>Enter a unique name for the UI component (e.g., "title-config", "axis-config")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.componentType" name="componentType" />
|
||||||
|
<clr-control-helper>Type of the component (e.g., "input", "select", "checkbox")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.displayLabel" name="displayLabel" />
|
||||||
|
<clr-control-helper>User-friendly label shown in the configuration form</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Placeholder</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newUiComponent.placeholder" name="placeholder" />
|
||||||
|
<clr-control-helper>Placeholder text for input fields</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="newUiComponent.sortOrder" name="sortOrder" />
|
||||||
|
<clr-control-helper>Order in which components appear in the form</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="newUiComponent.isRequired" name="isRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Mark as required if this component must be filled</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!newUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Create UI Component
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="showAddForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UI Components Grid -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="clr-row">
|
||||||
|
<div class="clr-col">
|
||||||
|
<h3>UI Components</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col" style="text-align: right;">
|
||||||
|
<button class="btn btn-primary" (click)="showAddForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add UI Component
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Component Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Component Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Display Label</clr-dg-column>
|
||||||
|
<clr-dg-column>Required</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let uiComponent of uiComponents" [clrDgItem]="uiComponent">
|
||||||
|
<clr-dg-cell>{{uiComponent.componentName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.componentType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{uiComponent.displayLabel}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<span class="label" [class.label-success]="uiComponent.isRequired" [class.label-danger]="!uiComponent.isRequired">
|
||||||
|
{{uiComponent.isRequired ? 'Yes' : 'No'}}
|
||||||
|
</span>
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-cell class="action-cell">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectUiComponentForEdit(uiComponent)" title="Edit" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="pencil" aria-label="Edit"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteUiComponent(uiComponent.id)" title="Delete" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="trash" aria-label="Delete"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon btn-primary" (click)="onViewComponentProperties(uiComponent)" title="View Properties" [disabled]="loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="loadingState !== ClrLoadingState.LOADING" shape="eye" aria-label="View Properties"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{uiComponents.length}} UI component(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit UI Component Form -->
|
||||||
|
<div class="card" *ngIf="selectedUiComponent && !showAddComponentPropertyForm && !selectedComponentProperty">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Edit UI Component</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="updateUiComponent()" #editUiComponentForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentName" name="editComponentName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Component Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.componentType" name="editComponentType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Label</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.displayLabel" name="editDisplayLabel" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Placeholder</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedUiComponent.placeholder" name="editPlaceholder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Sort Order</label>
|
||||||
|
<input clrInput type="number" [(ngModel)]="selectedUiComponent.sortOrder" name="editSortOrder" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Required</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="selectedUiComponent.isRequired" name="editIsRequired" />
|
||||||
|
<label>Required</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!selectedUiComponent.componentName || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update UI Component
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="selectedUiComponent = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Component Properties Section -->
|
||||||
|
<div class="card" *ngIf="selectedUiComponent">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="clr-row">
|
||||||
|
<div class="clr-col">
|
||||||
|
<h3>Properties for {{selectedUiComponent?.componentName}}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="clr-col" style="text-align: right;">
|
||||||
|
<button class="btn btn-primary" (click)="showAddComponentPropertyForm = true">
|
||||||
|
<cds-icon shape="plus"></cds-icon> Add Property
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="showAddComponentPropertyForm">
|
||||||
|
<form clrForm (ngSubmit)="createComponentProperty()" #addComponentPropertyForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyName" name="propertyName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Value</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyValue" name="propertyValue" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="newComponentProperty.propertyType" name="propertyType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!newComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Create Property
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="showAddComponentPropertyForm = false">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Component Properties Table -->
|
||||||
|
<div class="card-block">
|
||||||
|
<clr-datagrid [clrDgLoading]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-dg-column>Property Name</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Value</clr-dg-column>
|
||||||
|
<clr-dg-column>Property Type</clr-dg-column>
|
||||||
|
<clr-dg-column>Created At</clr-dg-column>
|
||||||
|
<clr-dg-column>Updated At</clr-dg-column>
|
||||||
|
<clr-dg-column>Actions</clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let property of componentProperties" [clrDgItem]="property">
|
||||||
|
<clr-dg-cell>{{property.propertyName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyValue}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.propertyType}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.createdAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>{{property.updatedAt | date:'short'}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="selectComponentPropertyForEdit(property)" title="Edit" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="pencil"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-icon" (click)="deleteComponentProperty(property.id)" title="Delete" [disabled]="componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
<cds-icon *ngIf="componentPropertyLoadingState !== ClrLoadingState.LOADING" shape="trash"></cds-icon>
|
||||||
|
</button>
|
||||||
|
</clr-dg-cell>
|
||||||
|
</clr-dg-row>
|
||||||
|
|
||||||
|
<clr-dg-footer>
|
||||||
|
{{componentProperties.length}} propertie(s)
|
||||||
|
</clr-dg-footer>
|
||||||
|
</clr-datagrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Component Property Form -->
|
||||||
|
<div class="card-block" *ngIf="selectedComponentProperty">
|
||||||
|
<h4>Edit Component Property</h4>
|
||||||
|
<form clrForm (ngSubmit)="updateComponentProperty()" #editComponentPropertyForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyName" name="editPropertyName" required />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Value</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyValue" name="editPropertyValue" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Property Type</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="selectedComponentProperty.propertyType" name="editPropertyType" />
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!selectedComponentProperty.propertyName || componentPropertyLoadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="componentPropertyLoadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update Property
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="selectedComponentProperty = null">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h4>About UI Components</h4>
|
||||||
|
<p>UI components define the configuration form elements for a chart type. Each component represents:</p>
|
||||||
|
<ul>
|
||||||
|
<li>A form field that appears when configuring a chart of this type</li>
|
||||||
|
<li>Metadata like label, placeholder, and validation rules</li>
|
||||||
|
<li>An order in which they appear in the configuration form</li>
|
||||||
|
</ul>
|
||||||
|
<p>After creating UI components, you can define their properties using the "View Properties" button.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
.chart-type-ui-components-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 0;
|
||||||
|
cds-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.label-success {
|
||||||
|
background-color: #3d9970;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-danger {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-datagrid {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
clr-dg-cell {
|
||||||
|
&:first-child {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-left: 4px solid #0079b8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component Properties Section Styles
|
||||||
|
.action-cell {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn-icon {
|
||||||
|
min-width: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure icons are visible
|
||||||
|
cds-icon {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
clr-spinner {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
.clr-row {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.clr-col {
|
||||||
|
text-align: left !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
import { UiComponentService } from '../chart-config/ui-component.service';
|
||||||
|
import { ComponentPropertyService } from '../chart-config/component-property.service';
|
||||||
|
import { UiComponent, ComponentProperty } from '../chart-config/chart-config-manager.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-type-ui-components',
|
||||||
|
templateUrl: './chart-type-ui-components.component.html',
|
||||||
|
styleUrls: ['./chart-type-ui-components.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartTypeUiComponentsComponent implements OnInit {
|
||||||
|
chartType: ChartType | null = null;
|
||||||
|
uiComponents: UiComponent[] = [];
|
||||||
|
newUiComponent: Partial<UiComponent> = {
|
||||||
|
isRequired: false
|
||||||
|
};
|
||||||
|
selectedUiComponent: UiComponent | null = null;
|
||||||
|
showAddForm = false;
|
||||||
|
|
||||||
|
// Component Properties
|
||||||
|
componentProperties: ComponentProperty[] = [];
|
||||||
|
selectedComponentProperty: ComponentProperty | null = null;
|
||||||
|
newComponentProperty: Partial<ComponentProperty> = {};
|
||||||
|
showAddComponentPropertyForm = false;
|
||||||
|
componentPropertyLoadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
|
||||||
|
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private uiComponentService: UiComponentService,
|
||||||
|
private componentPropertyService: ComponentPropertyService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const chartTypeId = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
|
if (chartTypeId) {
|
||||||
|
this.loadChartType(chartTypeId);
|
||||||
|
this.loadUiComponents(chartTypeId);
|
||||||
|
} else {
|
||||||
|
this.showError('Invalid chart type ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartType(id: number): void {
|
||||||
|
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.chartType = data;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart type:', error);
|
||||||
|
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUiComponents(chartTypeId: number): void {
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.getUiComponentsByChartType(chartTypeId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents = data;
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading UI components:', error);
|
||||||
|
this.showError('Error loading UI components: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createUiComponent(): void {
|
||||||
|
if (!this.chartType || !this.newUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Create a complete chartType object with only the ID (following the pattern in chart-config-manager)
|
||||||
|
const chartTypeWithId = {
|
||||||
|
id: this.chartType.id,
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
description: '',
|
||||||
|
isActive: true,
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentData = {
|
||||||
|
...this.newUiComponent,
|
||||||
|
chartType: chartTypeWithId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uiComponentService.createUiComponent(componentData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.uiComponents.push(data);
|
||||||
|
this.newUiComponent = { isRequired: false };
|
||||||
|
this.showAddForm = false;
|
||||||
|
this.showSuccess('UI component created successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating UI component:', error);
|
||||||
|
this.showError('Error creating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUiComponent(): void {
|
||||||
|
if (!this.selectedUiComponent || !this.selectedUiComponent.componentName) {
|
||||||
|
this.showError('Component name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.updateUiComponent(this.selectedUiComponent.id, this.selectedUiComponent).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.uiComponents.findIndex(uc => uc.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.uiComponents[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedUiComponent = null;
|
||||||
|
this.showSuccess('UI component updated successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating UI component:', error);
|
||||||
|
this.showError('Error updating UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteUiComponent(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this UI component?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.uiComponentService.deleteUiComponent(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.uiComponents = this.uiComponents.filter(uc => uc.id !== id);
|
||||||
|
this.showSuccess('UI component deleted successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting UI component:', error);
|
||||||
|
this.showError('Error deleting UI component: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUiComponentForEdit(uiComponent: UiComponent): void {
|
||||||
|
this.selectedUiComponent = { ...uiComponent };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component Property Methods
|
||||||
|
loadComponentProperties(componentId: number): void {
|
||||||
|
if (!componentId) {
|
||||||
|
this.componentProperties = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.getComponentPropertiesByComponent(componentId).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties = data;
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading component properties:', error);
|
||||||
|
this.showError('Error loading component properties: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createComponentProperty(): void {
|
||||||
|
if (!this.selectedUiComponent) {
|
||||||
|
this.showError('Please select a UI component first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.newComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
|
||||||
|
// Create a complete component object with only the ID
|
||||||
|
const componentWithId = {
|
||||||
|
id: this.selectedUiComponent.id
|
||||||
|
} as UiComponent;
|
||||||
|
|
||||||
|
const componentPropertyData = {
|
||||||
|
...this.newComponentProperty,
|
||||||
|
component: componentWithId
|
||||||
|
};
|
||||||
|
|
||||||
|
this.componentPropertyService.createComponentProperty(componentPropertyData).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
this.componentProperties.push(data);
|
||||||
|
this.newComponentProperty = {};
|
||||||
|
this.showAddComponentPropertyForm = false;
|
||||||
|
this.showSuccess('Component property created successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error creating component property:', error);
|
||||||
|
this.showError('Error creating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateComponentProperty(): void {
|
||||||
|
if (!this.selectedComponentProperty || !this.selectedComponentProperty.propertyName) {
|
||||||
|
this.showError('Property name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.updateComponentProperty(this.selectedComponentProperty.id, this.selectedComponentProperty).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
const index = this.componentProperties.findIndex(cp => cp.id === data.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.componentProperties[index] = data;
|
||||||
|
}
|
||||||
|
this.selectedComponentProperty = null;
|
||||||
|
this.showSuccess('Component property updated successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating component property:', error);
|
||||||
|
this.showError('Error updating component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteComponentProperty(id: number): void {
|
||||||
|
if (!confirm('Are you sure you want to delete this component property?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.LOADING;
|
||||||
|
this.componentPropertyService.deleteComponentProperty(id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.componentProperties = this.componentProperties.filter(cp => cp.id !== id);
|
||||||
|
this.showSuccess('Component property deleted successfully');
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting component property:', error);
|
||||||
|
this.showError('Error deleting component property: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.componentPropertyLoadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectComponentPropertyForEdit(componentProperty: ComponentProperty): void {
|
||||||
|
this.selectedComponentProperty = { ...componentProperty };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to view properties for a UI component
|
||||||
|
onViewComponentProperties(uiComponent: UiComponent): void {
|
||||||
|
this.selectedUiComponent = uiComponent;
|
||||||
|
this.loadComponentProperties(uiComponent.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack(): void {
|
||||||
|
if (this.chartType) {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types/edit', this.chartType.id]);
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ApiRequestService } from 'src/app/services/api/api-request.service';
|
||||||
|
|
||||||
|
export interface ChartType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ChartTypeService {
|
||||||
|
private chartTypesUrl = 'api/chart-types';
|
||||||
|
|
||||||
|
constructor(private apiRequest: ApiRequestService) { }
|
||||||
|
|
||||||
|
// Get all chart types
|
||||||
|
getAllChartTypes(): Observable<ChartType[]> {
|
||||||
|
console.log('Fetching all chart types from', this.chartTypesUrl);
|
||||||
|
return this.apiRequest.get(this.chartTypesUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chart type by ID
|
||||||
|
getChartTypeById(id: number): Observable<ChartType> {
|
||||||
|
const url = `${this.chartTypesUrl}/${id}`;
|
||||||
|
console.log(`Fetching chart type ${id} from ${url}`);
|
||||||
|
return this.apiRequest.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new chart type
|
||||||
|
createChartType(chartType: Partial<ChartType>): Observable<ChartType> {
|
||||||
|
console.log('Creating chart type:', chartType);
|
||||||
|
return this.apiRequest.post(this.chartTypesUrl, chartType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart type
|
||||||
|
updateChartType(id: number, chartType: ChartType): Observable<ChartType> {
|
||||||
|
const url = `${this.chartTypesUrl}/${id}`;
|
||||||
|
console.log(`Updating chart type ${id}:`, chartType);
|
||||||
|
return this.apiRequest.put(url, chartType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete chart type
|
||||||
|
deleteChartType(id: number): Observable<void> {
|
||||||
|
const url = `${this.chartTypesUrl}/${id}`;
|
||||||
|
console.log(`Deleting chart type ${id}`);
|
||||||
|
return this.apiRequest.delete(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<div class="edit-chart-type-page">
|
||||||
|
<div class="header">
|
||||||
|
<h2>Edit Chart Type</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div *ngIf="loadingState === ClrLoadingState.LOADING" class="loading-spinner">
|
||||||
|
<clr-spinner clrMedium></clr-spinner>
|
||||||
|
<span>Loading chart type...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error and Success Messages -->
|
||||||
|
<div class="alert alert-danger" *ngIf="errorMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="errorMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ errorMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" *ngIf="successMessage">
|
||||||
|
<button type="button" class="close" aria-label="Close" (click)="successMessage = null">
|
||||||
|
<cds-icon shape="close"></cds-icon>
|
||||||
|
</button>
|
||||||
|
<div class="alert-item">
|
||||||
|
<span class="alert-text">{{ successMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Type Form -->
|
||||||
|
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="chart-type-header">
|
||||||
|
<h3>{{ chartType.name }}</h3>
|
||||||
|
<span class="label" [class.label-success]="chartType.isActive" [class.label-danger]="!chartType.isActive">
|
||||||
|
{{ chartType.isActive ? 'Active' : 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<form clrForm (ngSubmit)="updateChartType()" #editChartTypeForm="ngForm">
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Name <span class="required">*</span></label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.name" name="chartTypeName" required />
|
||||||
|
<clr-control-helper>Enter a unique name for the chart type (e.g., "line-chart", "bar-chart")</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-input-container>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input clrInput type="text" [(ngModel)]="chartType.displayName" name="chartTypeDisplayName" />
|
||||||
|
<clr-control-helper>This is the user-friendly name shown in the UI</clr-control-helper>
|
||||||
|
</clr-input-container>
|
||||||
|
|
||||||
|
<clr-textarea-container>
|
||||||
|
<label>Description</label>
|
||||||
|
<textarea clrTextarea [(ngModel)]="chartType.description" name="chartTypeDescription" rows="3"></textarea>
|
||||||
|
<clr-control-helper>Provide a detailed description of this chart type and when to use it</clr-control-helper>
|
||||||
|
</clr-textarea-container>
|
||||||
|
|
||||||
|
<clr-checkbox-container>
|
||||||
|
<label>Is Active</label>
|
||||||
|
<clr-checkbox-wrapper>
|
||||||
|
<input type="checkbox" clrCheckbox [(ngModel)]="chartType.isActive" name="chartTypeIsActive" />
|
||||||
|
<label>Active</label>
|
||||||
|
</clr-checkbox-wrapper>
|
||||||
|
<clr-control-helper>Deactivate chart types that should not be available for selection</clr-control-helper>
|
||||||
|
</clr-checkbox-container>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn-primary" type="submit" [disabled]="!chartType.name || loadingState === ClrLoadingState.LOADING">
|
||||||
|
<clr-spinner *ngIf="loadingState === ClrLoadingState.LOADING" clrSmall clrInline></clr-spinner>
|
||||||
|
Update Chart Type
|
||||||
|
</button>
|
||||||
|
<button class="btn" type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
<button class="btn btn-danger" type="button" (click)="onDelete()" style="margin-left: auto;">
|
||||||
|
<cds-icon shape="trash"></cds-icon> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Related Entities Management -->
|
||||||
|
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Related Configuration</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<div class="related-entities-grid">
|
||||||
|
<div class="entity-card">
|
||||||
|
<div class="entity-header">
|
||||||
|
<cds-icon shape="view-list" size="24"></cds-icon>
|
||||||
|
<h4>UI Components</h4>
|
||||||
|
</div>
|
||||||
|
<p>Manage the UI components that define the configuration form for this chart type.</p>
|
||||||
|
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'ui-components']">
|
||||||
|
Manage UI Components
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="entity-card">
|
||||||
|
<div class="entity-header">
|
||||||
|
<cds-icon shape="template" size="24"></cds-icon>
|
||||||
|
<h4>Chart Templates</h4>
|
||||||
|
</div>
|
||||||
|
<p>Manage the templates that define how this chart type is rendered.</p>
|
||||||
|
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'templates']">
|
||||||
|
Manage Templates
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="entity-card">
|
||||||
|
<div class="entity-header">
|
||||||
|
<cds-icon shape="form" size="24"></cds-icon>
|
||||||
|
<h4>Dynamic Fields</h4>
|
||||||
|
</div>
|
||||||
|
<p>Manage the dynamic fields that capture specific configuration parameters.</p>
|
||||||
|
<button class="btn btn-sm btn-link" [routerLink]="['/cns-portal/dashboardbuilder/chart-types', chartType.id, 'fields']">
|
||||||
|
Manage Fields
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Type Details -->
|
||||||
|
<div class="card" *ngIf="chartType && loadingState !== ClrLoadingState.LOADING">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Chart Type Details</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-block">
|
||||||
|
<div class="details-grid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>ID:</label>
|
||||||
|
<span>{{ chartType.id }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>Created At:</label>
|
||||||
|
<span>{{ chartType.createdAt ? (chartType.createdAt | date:'medium') : 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<label>Updated At:</label>
|
||||||
|
<span>{{ chartType.updatedAt ? (chartType.updatedAt | date:'medium') : 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h4>About Chart Types</h4>
|
||||||
|
<p>Chart types define the different visualization options available in the dashboard builder. Each chart type can have:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Associated UI components that define the configuration form</li>
|
||||||
|
<li>Templates that define how the chart is rendered</li>
|
||||||
|
<li>Dynamic fields that capture specific configuration parameters</li>
|
||||||
|
</ul>
|
||||||
|
<p>After creating a chart type, you can configure its components, templates, and fields using the management links above.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
.edit-chart-type-page {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
h2 {
|
||||||
|
color: #0079b8;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.card-block {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
.chart-type-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-entities-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.entity-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
|
||||||
|
.entity-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
cds-icon {
|
||||||
|
color: #0079b8;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
border-left: 4px solid #0079b8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0079b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-entities-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ClrLoadingState } from '@clr/angular';
|
||||||
|
import { ChartType, ChartTypeService } from './chart-type.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit-chart-type',
|
||||||
|
templateUrl: './edit-chart-type.component.html',
|
||||||
|
styleUrls: ['./edit-chart-type.component.scss']
|
||||||
|
})
|
||||||
|
export class EditChartTypeComponent implements OnInit {
|
||||||
|
chartType: ChartType | null = null;
|
||||||
|
loadingState: ClrLoadingState = ClrLoadingState.DEFAULT;
|
||||||
|
errorMessage: string | null = null;
|
||||||
|
successMessage: string | null = null;
|
||||||
|
|
||||||
|
// Make ClrLoadingState available to template
|
||||||
|
readonly ClrLoadingState = ClrLoadingState;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private chartTypeService: ChartTypeService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const id = Number(this.route.snapshot.paramMap.get('id'));
|
||||||
|
if (id) {
|
||||||
|
this.loadChartType(id);
|
||||||
|
} else {
|
||||||
|
this.showError('Invalid chart type ID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
private showError(message: string): void {
|
||||||
|
this.errorMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMessage = null;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
private showSuccess(message: string): void {
|
||||||
|
this.successMessage = message;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.successMessage = null;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartType(id: number): void {
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.getChartTypeById(id).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
// Process the data to ensure dates are properly formatted
|
||||||
|
this.chartType = {
|
||||||
|
...data,
|
||||||
|
createdAt: this.formatDate(data.createdAt),
|
||||||
|
updatedAt: this.formatDate(data.updatedAt)
|
||||||
|
};
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart type:', error);
|
||||||
|
this.showError('Error loading chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date to handle both string and object formats
|
||||||
|
private formatDate(date: any): string {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
// If it's already a string, return as is
|
||||||
|
if (typeof date === 'string') {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an object, try to convert to string
|
||||||
|
if (typeof date === 'object') {
|
||||||
|
// Handle various date object formats
|
||||||
|
if (date instanceof Date) {
|
||||||
|
return date.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ISO string within object
|
||||||
|
if (date.date) {
|
||||||
|
return date.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle other object formats
|
||||||
|
try {
|
||||||
|
return new Date(date).toISOString();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChartType(): void {
|
||||||
|
if (!this.chartType || !this.chartType.name) {
|
||||||
|
this.showError('Chart type name is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.updateChartType(this.chartType.id, this.chartType).subscribe({
|
||||||
|
next: (data) => {
|
||||||
|
// Process the data to ensure dates are properly formatted
|
||||||
|
this.chartType = {
|
||||||
|
...data,
|
||||||
|
createdAt: this.formatDate(data.createdAt),
|
||||||
|
updatedAt: this.formatDate(data.updatedAt)
|
||||||
|
};
|
||||||
|
this.showSuccess('Chart type updated successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
// Redirect to chart types list after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error updating chart type:', error);
|
||||||
|
this.showError('Error updating chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel(): void {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDelete(): void {
|
||||||
|
if (!this.chartType) return;
|
||||||
|
|
||||||
|
if (confirm('Are you sure you want to delete this chart type? This action cannot be undone.')) {
|
||||||
|
this.loadingState = ClrLoadingState.LOADING;
|
||||||
|
this.chartTypeService.deleteChartType(this.chartType.id).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.showSuccess('Chart type deleted successfully');
|
||||||
|
this.loadingState = ClrLoadingState.SUCCESS;
|
||||||
|
// Redirect to chart types list after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
this.router.navigate(['/cns-portal/dashboardbuilder/chart-types']);
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error deleting chart type:', error);
|
||||||
|
this.showError('Error deleting chart type: ' + (error.error?.message || error.message || 'Unknown error'));
|
||||||
|
this.loadingState = ClrLoadingState.ERROR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="chart-config-modal">
|
||||||
|
<h3>Chart Configuration Manager</h3>
|
||||||
|
<app-chart-config-manager></app-chart-config-manager>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.chart-config-modal {
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-chart-config-modal',
|
||||||
|
templateUrl: './chart-config-modal.component.html',
|
||||||
|
styleUrls: ['./chart-config-modal.component.scss']
|
||||||
|
})
|
||||||
|
export class ChartConfigModalComponent {
|
||||||
|
// This component will be used to display the chart configuration manager in a modal
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
<div style="display: inline; float: right;">
|
<div style="display: inline; float: right;">
|
||||||
<!-- <button class="btn btn-primary">Build</button>
|
<!-- <button class="btn btn-primary">Build</button>
|
||||||
<button class="btn btn-primary" (click)="onSchedule()">Schedule</button> -->
|
<button class="btn btn-primary" (click)="onSchedule()">Schedule</button> -->
|
||||||
|
<button class="btn btn-success" (click)="testDynamicChartCreation()" style="margin-left: 10px;" *ngIf="!fromRunner">
|
||||||
|
<clr-icon shape="test"></clr-icon> Test Dynamic Chart
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ import { CommonFilterComponent } from '../common-filter/common-filter.component'
|
|||||||
import { CompactFilterComponent } from '../common-filter';
|
import { CompactFilterComponent } from '../common-filter';
|
||||||
// Add the FilterService import
|
// Add the FilterService import
|
||||||
import { FilterService } from '../common-filter/filter.service';
|
import { FilterService } from '../common-filter/filter.service';
|
||||||
|
// Add the UnifiedChartComponent import
|
||||||
|
import { UnifiedChartComponent } from '../gadgets/unified-chart';
|
||||||
|
// Add the DynamicChartLoaderService import
|
||||||
|
import { DynamicChartLoaderService } from '../chart-config/dynamic-chart-loader.service';
|
||||||
|
|
||||||
function isNullArray(arr) {
|
function isNullArray(arr) {
|
||||||
return !Array.isArray(arr) || arr.length === 0;
|
return !Array.isArray(arr) || arr.length === 0;
|
||||||
@@ -56,6 +60,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
// Add availableKeys property for compact filter
|
// Add availableKeys property for compact filter
|
||||||
availableKeys: string[] = [];
|
availableKeys: string[] = [];
|
||||||
|
|
||||||
|
// Initialize with default widgets and update dynamically
|
||||||
WidgetsMock: WidgetModel[] = [
|
WidgetsMock: WidgetModel[] = [
|
||||||
{
|
{
|
||||||
name: 'Common Filter',
|
name: 'Common Filter',
|
||||||
@@ -73,26 +78,26 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
name: 'Line Chart',
|
name: 'Line Chart',
|
||||||
identifier: 'line_chart'
|
identifier: 'line_chart'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'Bar Chart',
|
// name: 'Bar Chart',
|
||||||
identifier: 'bar_chart'
|
// identifier: 'bar_chart'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'Pie Chart',
|
// name: 'Pie Chart',
|
||||||
identifier: 'pie_chart'
|
// identifier: 'pie_chart'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'Polar Area Chart',
|
// name: 'Polar Area Chart',
|
||||||
identifier: 'polar_area_chart'
|
// identifier: 'polar_area_chart'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'Bubble Chart',
|
// name: 'Bubble Chart',
|
||||||
identifier: 'bubble_chart'
|
// identifier: 'bubble_chart'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: 'Scatter Chart',
|
// name: 'Scatter Chart',
|
||||||
identifier: 'scatter_chart'
|
// identifier: 'scatter_chart'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: 'Dynamic Chart',
|
name: 'Dynamic Chart',
|
||||||
identifier: 'dynamic_chart'
|
identifier: 'dynamic_chart'
|
||||||
@@ -112,6 +117,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
name: 'Compact Filter',
|
name: 'Compact Filter',
|
||||||
identifier: 'compact_filter'
|
identifier: 'compact_filter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unified Chart',
|
||||||
|
identifier: 'unified_chart'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -125,19 +134,20 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
|
|
||||||
protected componentCollection = [
|
protected componentCollection = [
|
||||||
{ name: "Common Filter", componentInstance: CommonFilterComponent },
|
{ name: "Common Filter", componentInstance: CommonFilterComponent },
|
||||||
{ name: "Line Chart", componentInstance: LineChartComponent },
|
{ name: "Line Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Doughnut Chart", componentInstance: DoughnutChartComponent },
|
{ name: "Doughnut Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Radar Chart", componentInstance: RadarChartComponent },
|
{ name: "Radar Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Bar Chart", componentInstance: BarChartComponent },
|
{ name: "Bar Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Pie Chart", componentInstance: PieChartComponent },
|
{ name: "Pie Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Polar Area Chart", componentInstance: PolarChartComponent },
|
{ name: "Polar Area Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Bubble Chart", componentInstance: BubbleChartComponent },
|
{ name: "Bubble Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Scatter Chart", componentInstance: ScatterChartComponent },
|
{ name: "Scatter Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Dynamic Chart", componentInstance: DynamicChartComponent },
|
{ name: "Dynamic Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "Financial Chart", componentInstance: FinancialChartComponent },
|
{ name: "Financial Chart", componentInstance: UnifiedChartComponent },
|
||||||
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
{ name: "To Do Chart", componentInstance: ToDoChartComponent },
|
||||||
{ name: "Grid View", componentInstance: GridViewComponent },
|
{ name: "Grid View", componentInstance: GridViewComponent },
|
||||||
{ name: "Compact Filter", componentInstance: CompactFilterComponent }, // Add this line
|
{ name: "Compact Filter", componentInstance: CompactFilterComponent },
|
||||||
|
{ name: "Unified Chart", componentInstance: UnifiedChartComponent },
|
||||||
];
|
];
|
||||||
model: any;
|
model: any;
|
||||||
linesdata: any;
|
linesdata: any;
|
||||||
@@ -170,6 +180,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
yAxis: '',
|
yAxis: '',
|
||||||
xAxis: '',
|
xAxis: '',
|
||||||
connection: '', // Add connection field
|
connection: '', // Add connection field
|
||||||
|
chartType: '', // Add chartType field
|
||||||
// Drilldown configuration properties (base level)
|
// Drilldown configuration properties (base level)
|
||||||
drilldownEnabled: false,
|
drilldownEnabled: false,
|
||||||
drilldownApiUrl: '',
|
drilldownApiUrl: '',
|
||||||
@@ -198,6 +209,9 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
// Add drilldown column data property
|
// Add drilldown column data property
|
||||||
drilldownColumnData = []; // Add drilldown column data property
|
drilldownColumnData = []; // Add drilldown column data property
|
||||||
|
|
||||||
|
// Add chart types property for dynamic chart selection
|
||||||
|
chartTypes: any[] = [];
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private dashboardService: Dashboard3Service,
|
private dashboardService: Dashboard3Service,
|
||||||
@@ -206,7 +220,8 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
private datastoreService: DatastoreService,
|
private datastoreService: DatastoreService,
|
||||||
private alertService: AlertsService,
|
private alertService: AlertsService,
|
||||||
private sureconnectService: SureconnectService,
|
private sureconnectService: SureconnectService,
|
||||||
private filterService: FilterService) { } // Add SureconnectService and FilterService to constructor
|
private filterService: FilterService,
|
||||||
|
private dynamicChartLoader: DynamicChartLoaderService) { } // Add SureconnectService, FilterService, and DynamicChartLoaderService to constructor
|
||||||
|
|
||||||
// Add property to track if coming from dashboard runner
|
// Add property to track if coming from dashboard runner
|
||||||
fromRunner: boolean = false;
|
fromRunner: boolean = false;
|
||||||
@@ -291,6 +306,9 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
apiUrl: ['']
|
apiUrl: ['']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load chart types for dynamic chart selection
|
||||||
|
this.loadChartTypesForSelection();
|
||||||
|
|
||||||
// Load sureconnect data first, then load dashboard data
|
// Load sureconnect data first, then load dashboard data
|
||||||
this.loadSureconnectData();
|
this.loadSureconnectData();
|
||||||
|
|
||||||
@@ -298,6 +316,37 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
this.loadCommonFilterData();
|
this.loadCommonFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to load all chart types for dynamic selection
|
||||||
|
loadChartTypesForSelection() {
|
||||||
|
console.log('Loading chart types for selection');
|
||||||
|
this.dynamicChartLoader.loadActiveChartTypes().subscribe({
|
||||||
|
next: (chartTypes) => {
|
||||||
|
console.log('Loaded chart types for selection:', chartTypes);
|
||||||
|
this.chartTypes = chartTypes;
|
||||||
|
|
||||||
|
// Convert each chart type to a WidgetModel
|
||||||
|
const newWidgets = chartTypes.map(ct => ({
|
||||||
|
name: ct.displayName || ct.name,
|
||||||
|
// identifier: ct.name.toLowerCase().replace(/\s+/g, '_')
|
||||||
|
identifier: `${ct.name.toLowerCase().replace(/\s+/g, '_')}_chart`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Filter out duplicates by identifier
|
||||||
|
const existingIds = new Set(this.WidgetsMock.map(w => w.identifier));
|
||||||
|
const uniqueNewWidgets = newWidgets.filter(w => !existingIds.has(w.identifier));
|
||||||
|
|
||||||
|
// Append unique new widgets to WidgetsMock
|
||||||
|
this.WidgetsMock = [...this.WidgetsMock, ...uniqueNewWidgets];
|
||||||
|
|
||||||
|
console.log('Updated WidgetsMock:', this.WidgetsMock);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart types for selection:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add method to load sureconnect data
|
// Add method to load sureconnect data
|
||||||
loadSureconnectData() {
|
loadSureconnectData() {
|
||||||
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||||
@@ -385,6 +434,27 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Map chart names to unified chart types
|
||||||
|
const chartTypeMap = {
|
||||||
|
'Radar Chart': 'radar',
|
||||||
|
'Line Chart': 'line',
|
||||||
|
'Doughnut Chart': 'doughnut',
|
||||||
|
'Bar Chart': 'bar',
|
||||||
|
'Pie Chart': 'pie',
|
||||||
|
'Polar Area Chart': 'polar',
|
||||||
|
'Bubble Chart': 'bubble',
|
||||||
|
'Scatter Chart': 'scatter',
|
||||||
|
'Dynamic Chart': 'line',
|
||||||
|
'Financial Chart': 'line'
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is a chart, set the chartType property
|
||||||
|
if (chartTypeMap.hasOwnProperty(dashboard.name)) {
|
||||||
|
dashboard.chartType = chartTypeMap[dashboard.name];
|
||||||
|
// Keep the original name instead of changing it to "Unified Chart"
|
||||||
|
// dashboard.name = "Unified Chart";
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure compact filter configuration properties are properly initialized
|
// Ensure compact filter configuration properties are properly initialized
|
||||||
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
|
if (dashboard.component === 'Compact Filter' || dashboard.name === 'Compact Filter') {
|
||||||
// Make sure all compact filter properties exist
|
// Make sure all compact filter properties exist
|
||||||
@@ -409,6 +479,30 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Map unified chart types back to chart names for serialization
|
||||||
|
const chartNameMap = {
|
||||||
|
'radar': 'Radar Chart',
|
||||||
|
'line': 'Line Chart',
|
||||||
|
'doughnut': 'Doughnut Chart',
|
||||||
|
'bar': 'Bar Chart',
|
||||||
|
'pie': 'Pie Chart',
|
||||||
|
'polar': 'Polar Area Chart',
|
||||||
|
'bubble': 'Bubble Chart',
|
||||||
|
'scatter': 'Scatter Chart'
|
||||||
|
// Removed hardcoded heatmap entry to make it fully dynamic
|
||||||
|
};
|
||||||
|
|
||||||
|
// If this is a unified chart, set the name back to the appropriate chart name
|
||||||
|
if (dashboard.name === 'Unified Chart' && dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType)) {
|
||||||
|
dashboard.name = chartNameMap[dashboard.chartType];
|
||||||
|
}
|
||||||
|
// Also handle the case where the chart already has the correct name
|
||||||
|
else if (dashboard.chartType && chartNameMap.hasOwnProperty(dashboard.chartType) &&
|
||||||
|
dashboard.name === chartNameMap[dashboard.chartType]) {
|
||||||
|
// The name is already correct, no need to change it
|
||||||
|
dashboard.component = "Unified Chart";
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure compact filter configuration properties are preserved
|
// Ensure compact filter configuration properties are preserved
|
||||||
if (dashboard.name === 'Compact Filter') {
|
if (dashboard.name === 'Compact Filter') {
|
||||||
// Make sure all compact filter properties exist
|
// Make sure all compact filter properties exist
|
||||||
@@ -420,6 +514,7 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add method to get available fields for a filter dropdown (excluding already selected fields)
|
// Add method to get available fields for a filter dropdown (excluding already selected fields)
|
||||||
getAvailableFields(filters: any[], currentIndex: number, allFields: string[]): string[] {
|
getAvailableFields(filters: any[], currentIndex: number, allFields: string[]): string[] {
|
||||||
if (!filters || !allFields) {
|
if (!filters || !allFields) {
|
||||||
@@ -451,9 +546,11 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
//this._ds.updateDashboard(this.dashboardId, parsed).subscribe();
|
//this._ds.updateDashboard(this.dashboardId, parsed).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
onDrop(ev) {
|
onDrop = (ev) => {
|
||||||
|
console.log("on drop event ", ev);
|
||||||
const componentType = ev.dataTransfer.getData("widgetIdentifier");
|
const componentType = ev.dataTransfer.getData("widgetIdentifier");
|
||||||
// Safely calculate maxChartId, handling cases where chartid might be NaN or missing
|
// Safely calculate maxChartId, handling cases where chartid might be NaN or missing
|
||||||
|
console.log("on drop ", componentType);
|
||||||
let maxChartId = 0;
|
let maxChartId = 0;
|
||||||
if (this.dashboardArray && this.dashboardArray.length > 0) {
|
if (this.dashboardArray && this.dashboardArray.length > 0) {
|
||||||
const validChartIds = this.dashboardArray
|
const validChartIds = this.dashboardArray
|
||||||
@@ -465,106 +562,28 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (componentType) {
|
switch (componentType) {
|
||||||
|
// Handle all chart types by converting them to unified charts
|
||||||
case "radar_chart":
|
case "radar_chart":
|
||||||
return this.dashboardArray.push({
|
// Use dynamic chart creation for all chart types
|
||||||
cols: 5,
|
return this.createDynamicChart('radar', maxChartId);
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: RadarChartComponent,
|
|
||||||
name: "Radar Chart"
|
|
||||||
});
|
|
||||||
case "line_chart":
|
case "line_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('line', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 7,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: LineChartComponent,
|
|
||||||
name: "Line Chart"
|
|
||||||
});
|
|
||||||
case "doughnut_chart":
|
case "doughnut_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('doughnut', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: DoughnutChartComponent,
|
|
||||||
name: "Doughnut Chart"
|
|
||||||
});
|
|
||||||
case "bar_chart":
|
case "bar_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('bar', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: BarChartComponent,
|
|
||||||
name: "Bar Chart"
|
|
||||||
});
|
|
||||||
case "pie_chart":
|
case "pie_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('pie', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: PieChartComponent,
|
|
||||||
name: "Pie Chart"
|
|
||||||
});
|
|
||||||
case "polar_area_chart":
|
case "polar_area_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('polar', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: PolarChartComponent,
|
|
||||||
name: "Polar Area Chart"
|
|
||||||
});
|
|
||||||
case "bubble_chart":
|
case "bubble_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('bubble', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: BubbleChartComponent,
|
|
||||||
name: "Bubble Chart"
|
|
||||||
});
|
|
||||||
case "scatter_chart":
|
case "scatter_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('scatter', maxChartId);
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: ScatterChartComponent,
|
|
||||||
name: "Scatter Chart"
|
|
||||||
});
|
|
||||||
case "dynamic_chart":
|
case "dynamic_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('line', maxChartId); // Default to line for dynamic chart
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: DynamicChartComponent,
|
|
||||||
name: "Dynamic Chart"
|
|
||||||
});
|
|
||||||
case "financial_chart":
|
case "financial_chart":
|
||||||
return this.dashboardArray.push({
|
return this.createDynamicChart('line', maxChartId); // Default to line for financial chart
|
||||||
cols: 5,
|
|
||||||
rows: 6,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
chartid: maxChartId + 1,
|
|
||||||
component: FinancialChartComponent,
|
|
||||||
name: "Financial Chart"
|
|
||||||
});
|
|
||||||
case "to_do_chart":
|
case "to_do_chart":
|
||||||
return this.dashboardArray.push({
|
return this.dashboardArray.push({
|
||||||
cols: 5,
|
cols: 5,
|
||||||
@@ -610,6 +629,17 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
component: GridViewComponent,
|
component: GridViewComponent,
|
||||||
name: "Grid View"
|
name: "Grid View"
|
||||||
});
|
});
|
||||||
|
case "unified_chart":
|
||||||
|
return this.createDynamicChart('bar', maxChartId); // Default to bar for unified chart
|
||||||
|
default:
|
||||||
|
// Handle any other chart types dynamically
|
||||||
|
// Extract chart type name from identifier (e.g., "heatmap_chart" -> "heatmap")
|
||||||
|
const chartTypeName = componentType.replace('_chart', '');
|
||||||
|
console.log('Creating dynamic chart of type:', chartTypeName);
|
||||||
|
console.log('Display name for chart:', this.getChartDisplayName(chartTypeName));
|
||||||
|
|
||||||
|
// Use dynamic chart creation for all chart types
|
||||||
|
return this.createDynamicChart(chartTypeName, maxChartId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
removeItem(item) {
|
removeItem(item) {
|
||||||
@@ -662,6 +692,10 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
if (item['filterOptions'] === undefined) {
|
if (item['filterOptions'] === undefined) {
|
||||||
this.gadgetsEditdata['filterOptions'] = [];
|
this.gadgetsEditdata['filterOptions'] = [];
|
||||||
}
|
}
|
||||||
|
// Initialize chartType property if not present (for unified chart)
|
||||||
|
if (item['chartType'] === undefined) {
|
||||||
|
this.gadgetsEditdata['chartType'] = 'bar';
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize filterOptionsString for compact filter
|
// Initialize filterOptionsString for compact filter
|
||||||
if (item.name === 'Compact Filter') {
|
if (item.name === 'Compact Filter') {
|
||||||
@@ -930,6 +964,11 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
xyz.connection = this.gadgetsEditdata.connection || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For unified chart, preserve chart configuration properties
|
||||||
|
if (item.name === 'Unified Chart') {
|
||||||
|
xyz.chartType = this.gadgetsEditdata.chartType || 'bar';
|
||||||
|
}
|
||||||
|
|
||||||
console.log(xyz);
|
console.log(xyz);
|
||||||
return xyz;
|
return xyz;
|
||||||
}
|
}
|
||||||
@@ -1013,6 +1052,51 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
return commonFilterInputs;
|
return commonFilterInputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For UnifiedChartComponent, pass chart properties with chartType
|
||||||
|
// Check if the component is UnifiedChartComponent dynamically
|
||||||
|
if (item.component === UnifiedChartComponent ||
|
||||||
|
(item.component && item.component.name === 'UnifiedChartComponent') ||
|
||||||
|
item.name === 'Unified Chart') {
|
||||||
|
const unifiedChartInputs = {
|
||||||
|
chartType: item.chartType || 'bar',
|
||||||
|
xAxis: item.xAxis,
|
||||||
|
yAxis: item.yAxis,
|
||||||
|
table: item.table,
|
||||||
|
datastore: item.datastore,
|
||||||
|
charttitle: item.charttitle,
|
||||||
|
chartlegend: item.chartlegend,
|
||||||
|
showlabel: item.showlabel,
|
||||||
|
chartcolor: item.chartcolor,
|
||||||
|
slices: item.slices,
|
||||||
|
donut: item.donut,
|
||||||
|
charturl: item.charturl,
|
||||||
|
chartparameter: item.chartparameter,
|
||||||
|
datasource: item.datasource,
|
||||||
|
fieldName: item.name, // Using item.name as fieldName
|
||||||
|
connection: item['connection'], // Add connection field using bracket notation
|
||||||
|
// Base drilldown configuration properties
|
||||||
|
drilldownEnabled: item['drilldownEnabled'],
|
||||||
|
drilldownApiUrl: item['drilldownApiUrl'],
|
||||||
|
// Removed drilldownParameterKey since we're using URL templates
|
||||||
|
drilldownXAxis: item['drilldownXAxis'],
|
||||||
|
drilldownYAxis: item['drilldownYAxis'],
|
||||||
|
drilldownParameter: item['drilldownParameter'], // Add drilldown parameter
|
||||||
|
baseFilters: item['baseFilters'] || [], // Add base filters
|
||||||
|
drilldownFilters: item['drilldownFilters'] || [], // Add drilldown filters
|
||||||
|
// Multi-layer drilldown configurations
|
||||||
|
drilldownLayers: item['drilldownLayers'] || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove undefined properties to avoid passing unnecessary data
|
||||||
|
Object.keys(unifiedChartInputs).forEach(key => {
|
||||||
|
if (unifiedChartInputs[key] === undefined) {
|
||||||
|
delete unifiedChartInputs[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return unifiedChartInputs;
|
||||||
|
}
|
||||||
|
|
||||||
// For GridViewComponent, pass chart properties with drilldown support
|
// For GridViewComponent, pass chart properties with drilldown support
|
||||||
if (item.component && item.component.name === 'GridViewComponent') {
|
if (item.component && item.component.name === 'GridViewComponent') {
|
||||||
const gridInputs = {
|
const gridInputs = {
|
||||||
@@ -1867,4 +1951,266 @@ export class EditnewdashComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to apply dynamic template to a chart
|
||||||
|
applyDynamicTemplate(chartItem: any, template: any) {
|
||||||
|
console.log('Applying dynamic template to chart:', chartItem, template);
|
||||||
|
|
||||||
|
// Apply HTML template
|
||||||
|
if (template.templateHtml) {
|
||||||
|
// In a real implementation, you would dynamically render the HTML template
|
||||||
|
// For now, we'll just log it
|
||||||
|
console.log('HTML Template:', template.templateHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply CSS styles
|
||||||
|
if (template.templateCss) {
|
||||||
|
// In a real implementation, you would dynamically apply the CSS styles
|
||||||
|
// For now, we'll just log it
|
||||||
|
console.log('CSS Template:', template.templateCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the chart item with template applied
|
||||||
|
return {
|
||||||
|
...chartItem,
|
||||||
|
template: template
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to test dynamic chart creation
|
||||||
|
testDynamicChartCreation() {
|
||||||
|
console.log('Testing dynamic chart creation');
|
||||||
|
|
||||||
|
// Show a success message to the user
|
||||||
|
alert('Dynamic chart test started. Check the browser console for detailed output.');
|
||||||
|
|
||||||
|
// Load all chart types
|
||||||
|
this.dynamicChartLoader.loadAllChartConfigurations().subscribe({
|
||||||
|
next: (chartTypes) => {
|
||||||
|
console.log('Loaded chart types:', chartTypes);
|
||||||
|
|
||||||
|
// Find bar chart type
|
||||||
|
const barChartType = chartTypes.find((ct: any) => ct.name === 'bar');
|
||||||
|
if (barChartType) {
|
||||||
|
console.log('Found bar chart type:', barChartType);
|
||||||
|
|
||||||
|
// Load configuration for bar chart
|
||||||
|
this.dynamicChartLoader.loadChartConfiguration(barChartType.id).subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
console.log('Loaded bar chart configuration:', config);
|
||||||
|
|
||||||
|
// Create a test chart item
|
||||||
|
const chartItem = {
|
||||||
|
cols: 5,
|
||||||
|
rows: 6,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
chartid: 100,
|
||||||
|
component: UnifiedChartComponent,
|
||||||
|
name: 'Test Dynamic Bar Chart',
|
||||||
|
chartType: 'bar',
|
||||||
|
xAxis: '',
|
||||||
|
yAxis: '',
|
||||||
|
table: '',
|
||||||
|
connection: undefined,
|
||||||
|
// Add dynamic fields from configuration
|
||||||
|
dynamicFields: config.dynamicFields || []
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Created test chart item:', chartItem);
|
||||||
|
|
||||||
|
// If we have templates, apply the default one
|
||||||
|
if (config.templates && config.templates.length > 0) {
|
||||||
|
const defaultTemplate = config.templates.find((t: any) => t.isDefault) || config.templates[0];
|
||||||
|
if (defaultTemplate) {
|
||||||
|
console.log('Applying default template:', defaultTemplate);
|
||||||
|
const chartWithTemplate = this.applyDynamicTemplate(chartItem, defaultTemplate);
|
||||||
|
console.log('Chart with template:', chartWithTemplate);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert('Dynamic chart test completed successfully! Check console for details.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show success message even without templates
|
||||||
|
alert('Dynamic chart test completed successfully! No templates found. Check console for details.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading bar chart configuration:', error);
|
||||||
|
alert('Error loading bar chart configuration. Check console for details.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Bar chart type not found');
|
||||||
|
alert('Bar chart type not found in the database.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading chart types:', error);
|
||||||
|
alert('Error loading chart types. Check console for details.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to load dynamic chart configuration
|
||||||
|
loadDynamicChartConfiguration(chartTypeId: number) {
|
||||||
|
console.log(`Loading dynamic chart configuration for chart type ${chartTypeId}`);
|
||||||
|
this.dynamicChartLoader.loadChartConfiguration(chartTypeId).subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
console.log('Loaded dynamic chart configuration:', config);
|
||||||
|
// Here you would apply the configuration to the UI
|
||||||
|
// For example, populate form fields, set up templates, etc.
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading dynamic chart configuration:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Add method to create a dynamic chart with configuration from database
|
||||||
|
createDynamicChart = (chartTypeName: string, maxChartId: number) => {
|
||||||
|
console.log(`Creating dynamic chart of type: ${chartTypeName}`);
|
||||||
|
|
||||||
|
// First, get the chart type by name
|
||||||
|
this.dynamicChartLoader.getChartTypeByName(chartTypeName).subscribe({
|
||||||
|
next: (chartType) => {
|
||||||
|
if (chartType) {
|
||||||
|
console.log(`Found chart type:`, chartType);
|
||||||
|
|
||||||
|
// Load the complete configuration for this chart type
|
||||||
|
this.dynamicChartLoader.loadChartConfiguration(chartType.id).subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
console.log(`Loaded configuration for ${chartTypeName}:`, config);
|
||||||
|
|
||||||
|
// Create the chart item with dynamic configuration
|
||||||
|
const chartItem = {
|
||||||
|
cols: 5,
|
||||||
|
rows: 6,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
chartid: maxChartId + 1,
|
||||||
|
component: UnifiedChartComponent,
|
||||||
|
name: chartType.displayName || chartTypeName,
|
||||||
|
chartType: chartType.name,
|
||||||
|
xAxis: '',
|
||||||
|
yAxis: '',
|
||||||
|
table: '',
|
||||||
|
connection: undefined,
|
||||||
|
// Add any dynamic fields from the configuration
|
||||||
|
dynamicFields: config.dynamicFields || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add UI components as configuration properties
|
||||||
|
if (config.uiComponents && config.uiComponents.length > 0) {
|
||||||
|
config.uiComponents.forEach(component => {
|
||||||
|
chartItem[component.componentName] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dashboardArray.push(chartItem);
|
||||||
|
console.log(`Created dynamic chart:`, chartItem);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error(`Error loading configuration for ${chartTypeName}:`, error);
|
||||||
|
// Fallback to default configuration
|
||||||
|
this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`Chart type ${chartTypeName} not found, using default configuration`);
|
||||||
|
this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error loading configuration for chart type:', error);
|
||||||
|
// Fallback to default configuration
|
||||||
|
this.createDefaultChart(chartTypeName, this.getChartDisplayName(chartTypeName));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback method to create default chart configuration
|
||||||
|
createDefaultChart = (chartTypeName: string, chartDisplayName: string) => {
|
||||||
|
console.log(`Creating default chart for ${chartTypeName}`);
|
||||||
|
|
||||||
|
// Map chart type names to chart types - making it fully dynamic
|
||||||
|
const chartTypeMap = {
|
||||||
|
'bar': 'bar',
|
||||||
|
'line': 'line',
|
||||||
|
'pie': 'pie',
|
||||||
|
'doughnut': 'doughnut',
|
||||||
|
'radar': 'radar',
|
||||||
|
'polar': 'polar',
|
||||||
|
'bubble': 'bubble',
|
||||||
|
'scatter': 'scatter'
|
||||||
|
// Removed hardcoded heatmap entry to make it fully dynamic
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the chart type from the name - default to bubble for unknown chart types
|
||||||
|
const chartType = chartTypeMap[chartTypeName.toLowerCase()] || 'bubble';
|
||||||
|
|
||||||
|
// Safely calculate maxChartId, handling cases where chartid might be NaN or missing
|
||||||
|
let maxChartId = 0;
|
||||||
|
if (this.dashboardArray && this.dashboardArray.length > 0) {
|
||||||
|
const validChartIds = this.dashboardArray
|
||||||
|
.map(item => item.chartid)
|
||||||
|
.filter(chartid => typeof chartid === 'number' && !isNaN(chartid));
|
||||||
|
|
||||||
|
if (validChartIds.length > 0) {
|
||||||
|
maxChartId = Math.max(...validChartIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartItem = {
|
||||||
|
cols: 5,
|
||||||
|
rows: 6,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
chartid: maxChartId + 1,
|
||||||
|
component: UnifiedChartComponent,
|
||||||
|
name: chartDisplayName,
|
||||||
|
chartType: chartType,
|
||||||
|
xAxis: '',
|
||||||
|
yAxis: '',
|
||||||
|
table: '',
|
||||||
|
connection: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dashboardArray.push(chartItem);
|
||||||
|
console.log('Created default chart:', chartItem);
|
||||||
|
|
||||||
|
// Update the dashboard collection
|
||||||
|
this.dashboardCollection.dashboard = this.dashboardArray.slice();
|
||||||
|
|
||||||
|
// Force gridster to refresh
|
||||||
|
if (this.options && this.options.api) {
|
||||||
|
this.options.api.optionsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get display name for chart type - making it fully dynamic
|
||||||
|
getChartDisplayName = (chartTypeName: string): string => {
|
||||||
|
const displayNameMap = {
|
||||||
|
'bar': 'Bar Chart',
|
||||||
|
'line': 'Line Chart',
|
||||||
|
'pie': 'Pie Chart',
|
||||||
|
'doughnut': 'Doughnut Chart',
|
||||||
|
'radar': 'Radar Chart',
|
||||||
|
'polar': 'Polar Area Chart',
|
||||||
|
'bubble': 'Bubble Chart',
|
||||||
|
'scatter': 'Scatter Chart'
|
||||||
|
// Removed hardcoded heatmap entry to make it fully dynamic
|
||||||
|
};
|
||||||
|
|
||||||
|
// For unknown chart types, create a display name by capitalizing the first letter and adding ' Chart'
|
||||||
|
const displayName = displayNameMap[chartTypeName.toLowerCase()];
|
||||||
|
if (displayName) {
|
||||||
|
return displayName;
|
||||||
|
} else {
|
||||||
|
// Capitalize first letter and add ' Chart'
|
||||||
|
return chartTypeName.charAt(0).toUpperCase() + chartTypeName.slice(1) + ' Chart';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './unified-chart.component';
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
<div class="chart-container" *ngIf="!noDataAvailable && !isLoading">
|
||||||
|
<!-- Back button for drilldown navigation -->
|
||||||
|
<div class="drilldown-back" *ngIf="currentDrilldownLevel > 0">
|
||||||
|
<button class="btn btn-sm btn-secondary" (click)="navigateBack()">
|
||||||
|
<clr-icon shape="arrow" dir="left"></clr-icon>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<span class="drilldown-level">Level: {{ currentDrilldownLevel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart title -->
|
||||||
|
<div class="chart-title" *ngIf="charttitle">
|
||||||
|
<h4>{{ charttitle }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter toggle icon -->
|
||||||
|
<div class="filter-toggle-icon" *ngIf="hasFilters()" (click)="toggleFilters()">
|
||||||
|
<clr-icon shape="filter" size="24"
|
||||||
|
[style.color]="showFilters ? '#007cba' : '#666'"
|
||||||
|
title="Toggle Filters">
|
||||||
|
</clr-icon>
|
||||||
|
<span>
|
||||||
|
{{ showFilters ? 'Hide Filters' : 'Show Filters' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Render different chart types based on chartType input -->
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<!-- Dynamic Chart Container - uses extracted dynamic options and styles but static template -->
|
||||||
|
<div *ngIf="dynamicTemplate || chartType" class="chart-canvas-container">
|
||||||
|
<canvas baseChart
|
||||||
|
[datasets]="chartData"
|
||||||
|
[labels]="chartLabels"
|
||||||
|
[options]="chartOptions"
|
||||||
|
[legend]="chartLegend"
|
||||||
|
[chartType]="chartType || 'bar'"
|
||||||
|
(chartClick)="chartClicked($event)"
|
||||||
|
(chartHover)="chartHovered($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback for when no chart type is specified -->
|
||||||
|
<div *ngIf="!dynamicTemplate && !chartType" class="chart-canvas-container">
|
||||||
|
<canvas baseChart
|
||||||
|
[datasets]="chartData"
|
||||||
|
[labels]="chartLabels"
|
||||||
|
[options]="chartOptions"
|
||||||
|
[legend]="chartLegend"
|
||||||
|
[chartType]="'bar'"
|
||||||
|
(chartClick)="chartClicked($event)"
|
||||||
|
(chartHover)="chartHovered($event)">
|
||||||
|
</canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsible Base Filters -->
|
||||||
|
<div class="filters-section" *ngIf="baseFilters && baseFilters.length > 0 && showFilters">
|
||||||
|
<h5>Filters</h5>
|
||||||
|
<div class="filters-container">
|
||||||
|
<div class="filter-item" *ngFor="let filter of baseFilters; let i = index">
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)"
|
||||||
|
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<select [(ngModel)]="filter.value" (ngModelChange)="onBaseFilterChange(filter)" class="form-control">
|
||||||
|
<option value="">Select {{ filter.field || 'value' }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiselect Filter -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'base')">
|
||||||
|
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
|
||||||
|
<span *ngIf="filter.value && !Array.isArray(filter.value)">
|
||||||
|
{{ filter.value }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
|
||||||
|
{{ filter.value.length }} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'base')">
|
||||||
|
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
id="base-{{ filter.field }}-{{ option }}">
|
||||||
|
<label [for]="'base-' + filter.field + '-' + option">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="date-range-inputs">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
|
||||||
|
class="form-control" placeholder="Start Date">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
|
||||||
|
class="form-control" placeholder="End Date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
|
||||||
|
id="toggle-{{ filter.field }}">
|
||||||
|
<label [for]="'toggle-' + filter.field" class="toggle-label">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drilldown Filters -->
|
||||||
|
<div class="filters-section" *ngIf="drilldownFilters && drilldownFilters.length > 0 && currentDrilldownLevel > 0 && showFilters">
|
||||||
|
<h5>Drilldown Filters</h5>
|
||||||
|
<div class="filters-container">
|
||||||
|
<div class="filter-item" *ngFor="let filter of drilldownFilters; let i = index">
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)"
|
||||||
|
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<select [(ngModel)]="filter.value" (ngModelChange)="onDrilldownFilterChange(filter)" class="form-control">
|
||||||
|
<option value="">Select {{ filter.field || 'value' }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiselect Filter -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'drilldown')">
|
||||||
|
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
|
||||||
|
<span *ngIf="filter.value && !Array.isArray(filter.value)">
|
||||||
|
{{ filter.value }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
|
||||||
|
{{ filter.value.length }} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'drilldown')">
|
||||||
|
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
id="drilldown-{{ filter.field }}-{{ option }}">
|
||||||
|
<label [for]="'drilldown-' + filter.field + '-' + option">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="date-range-inputs">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
|
||||||
|
class="form-control" placeholder="Start Date">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
|
||||||
|
class="form-control" placeholder="End Date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
|
||||||
|
id="drilldown-toggle-{{ filter.field }}">
|
||||||
|
<label [for]="'drilldown-toggle-' + filter.field" class="toggle-label">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Filters -->
|
||||||
|
<div class="filters-section" *ngIf="hasActiveLayerFilters() && showFilters">
|
||||||
|
<h5>Layer Filters</h5>
|
||||||
|
<div class="filters-container">
|
||||||
|
<div class="filter-item" *ngFor="let filter of getActiveLayerFilters(); let i = index">
|
||||||
|
<!-- Text Filter -->
|
||||||
|
<div *ngIf="!filter.type || filter.type === 'text'" class="filter-text">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<input type="text" [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)"
|
||||||
|
class="form-control" placeholder="Enter {{ filter.field || 'value' }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown Filter -->
|
||||||
|
<div *ngIf="filter.type === 'dropdown'" class="filter-dropdown">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<select [(ngModel)]="filter.value" (ngModelChange)="onLayerFilterChange(filter)" class="form-control">
|
||||||
|
<option value="">Select {{ filter.field || 'value' }}</option>
|
||||||
|
<option *ngFor="let option of getFilterOptions(filter)" [value]="option">
|
||||||
|
{{ option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiselect Filter -->
|
||||||
|
<div *ngIf="filter.type === 'multiselect'" class="filter-multiselect">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="multiselect-container">
|
||||||
|
<div class="multiselect-display" (click)="toggleMultiselect(filter, 'layer')">
|
||||||
|
<span *ngIf="!filter.value || (Array.isArray(filter.value) && filter.value.length === 0)">Select {{ filter.field || 'options' }}</span>
|
||||||
|
<span *ngIf="filter.value && !Array.isArray(filter.value)">
|
||||||
|
{{ filter.value }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="filter.value && Array.isArray(filter.value) && filter.value.length > 0">
|
||||||
|
{{ filter.value.length }} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="multiselect-dropdown" *ngIf="isMultiselectOpen(filter, 'layer')">
|
||||||
|
<div class="multiselect-option" *ngFor="let option of getFilterOptions(filter)">
|
||||||
|
<input type="checkbox"
|
||||||
|
[checked]="isOptionSelected(filter, option)"
|
||||||
|
(change)="onMultiSelectChange(filter, option, $event)"
|
||||||
|
id="layer-{{ filter.field }}-{{ option }}">
|
||||||
|
<label [for]="'layer-' + filter.field + '-' + option">{{ option }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Filter -->
|
||||||
|
<div *ngIf="filter.type === 'date-range'" class="filter-date-range">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="date-range-inputs">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.start" (ngModelChange)="onDateRangeInputChange(filter, 'start', $event)"
|
||||||
|
class="form-control" placeholder="Start Date">
|
||||||
|
<input type="date" [(ngModel)]="filter.value.end" (ngModelChange)="onDateRangeInputChange(filter, 'end', $event)"
|
||||||
|
class="form-control" placeholder="End Date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggle Filter -->
|
||||||
|
<div *ngIf="filter.type === 'toggle'" class="filter-toggle">
|
||||||
|
<label>{{ filter.field || 'Filter ' + (i + 1) }}</label>
|
||||||
|
<div class="toggle-switch">
|
||||||
|
<input type="checkbox" [(ngModel)]="filter.value" (ngModelChange)="onToggleChange(filter, $event.target.checked)"
|
||||||
|
id="layer-toggle-{{ filter.field }}">
|
||||||
|
<label [for]="'layer-toggle-' + filter.field" class="toggle-label">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clear Filters Button -->
|
||||||
|
<div class="clear-filters" *ngIf="hasActiveFilters() && showFilters">
|
||||||
|
<button class="btn btn-sm btn-outline" (click)="clearAllFilters()">
|
||||||
|
Clear All Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No Data Available Message -->
|
||||||
|
<div class="no-data-message" *ngIf="noDataAvailable && !isLoading">
|
||||||
|
<p>No data available for the selected filters.</p>
|
||||||
|
<button class="btn btn-sm btn-primary" (click)="fetchChartData()">Retry</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Indicator -->
|
||||||
|
<div class="loading-indicator" *ngIf="isLoading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Loading chart data...</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drilldown-back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.drilldown-level {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas-container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
clr-icon {
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #007cba !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover span {
|
||||||
|
color: #007cba;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-section {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
flex: 1 1 200px;
|
||||||
|
min-width: 150px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text,
|
||||||
|
.filter-dropdown,
|
||||||
|
.filter-date-range {
|
||||||
|
.form-control {
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-multiselect {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.multiselect-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-display {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiselect-option {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.toggle-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
transition: .4s;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-label {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .toggle-label .toggle-slider {
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 300px;
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filters-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-canvas-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { UnifiedChartComponent } from './unified-chart.component';
|
||||||
|
|
||||||
|
describe('UnifiedChartComponent', () => {
|
||||||
|
let component: UnifiedChartComponent;
|
||||||
|
let fixture: ComponentFixture<UnifiedChartComponent>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [UnifiedChartComponent]
|
||||||
|
});
|
||||||
|
fixture = TestBed.createComponent(UnifiedChartComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -254,7 +254,7 @@ export class ReportbuildqueryComponent implements OnInit {
|
|||||||
name;
|
name;
|
||||||
databaseName;
|
databaseName;
|
||||||
databasename(val) {
|
databasename(val) {
|
||||||
console.log(val);
|
console.log('connection ', val);
|
||||||
this.databaseName = val.name;
|
this.databaseName = val.name;
|
||||||
this.selecteddatabase = val.conn_string;
|
this.selecteddatabase = val.conn_string;
|
||||||
console.log(this.selecteddatabase);
|
console.log(this.selecteddatabase);
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
<label for="workflow_name">{{'ACTIVE'| translate}}</label>
|
<label for="workflow_name">{{'ACTIVE'| translate}}</label>
|
||||||
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
|
<input type="checkbox" formControlName="active" clrToggle value="billable" name="billable" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SureConnect Dropdown -->
|
||||||
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
|
<label for="sureConnectId">SureConnect Connection</label>
|
||||||
|
<select formControlName="sureConnectId" class="clr-select">
|
||||||
|
<option value="">-- Select SureConnect --</option>
|
||||||
|
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||||
|
{{conn.connection_name || conn.id}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="url">Get URL</label>
|
<label for="url">Get URL</label>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
||||||
|
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-report-build2add',
|
selector: 'app-report-build2add',
|
||||||
templateUrl: './report-build2add.component.html',
|
templateUrl: './report-build2add.component.html',
|
||||||
@@ -11,8 +13,12 @@ import { ReportBuilderService } from 'src/app/services/api/report-builder.servic
|
|||||||
})
|
})
|
||||||
export class ReportBuild2addComponent implements OnInit {
|
export class ReportBuild2addComponent implements OnInit {
|
||||||
public entryForm: FormGroup;
|
public entryForm: FormGroup;
|
||||||
|
// Add sureconnect data property
|
||||||
|
sureconnectData: any[] = [];
|
||||||
|
|
||||||
constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService,
|
constructor(private _fb: FormBuilder, private router: Router,private toastr: ToastrService,
|
||||||
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService) { }
|
private route: ActivatedRoute,private reportBuilderService: ReportBuilderService,
|
||||||
|
private sureconnectService: SureconnectService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.entryForm = this._fb.group({
|
this.entryForm = this._fb.group({
|
||||||
@@ -20,9 +26,23 @@ export class ReportBuild2addComponent implements OnInit {
|
|||||||
description:[null],
|
description:[null],
|
||||||
active:[null],
|
active:[null],
|
||||||
isSql:[false],
|
isSql:[false],
|
||||||
|
// Add sureConnectId field to the form
|
||||||
|
sureConnectId: [null],
|
||||||
Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]),
|
Rpt_builder2_lines: this._fb.array([this.initLinesFormReport()]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load sureconnect data
|
||||||
|
this.loadSureconnectData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add method to load sureconnect data
|
||||||
|
loadSureconnectData() {
|
||||||
|
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||||
|
this.sureconnectData = data;
|
||||||
|
console.log('Sureconnect data loaded:', this.sureconnectData);
|
||||||
|
}, (error) => {
|
||||||
|
console.log('Error loading sureconnect data:', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initLinesFormReport() {
|
initLinesFormReport() {
|
||||||
|
|||||||
@@ -6,70 +6,87 @@
|
|||||||
|
|
||||||
<div class="dg-wrapper">
|
<div class="dg-wrapper">
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<div class="clr-col-4">
|
<div class="clr-col-4">
|
||||||
<h3><b>{{'REPORT_BUILDER_2' | translate}}</b></h3>
|
<h3><b>{{'REPORT_BUILDER_2' | translate}}</b></h3>
|
||||||
</div>
|
|
||||||
<div class="clr-col-8" style="text-align: right;">
|
|
||||||
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
|
||||||
<clr-icon shape="grid-view"></clr-icon>{{'REPORT_RUNNER' | translate}}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="add" class="btn btn-primary" (click)="goToAdd()">
|
|
||||||
<clr-icon shape="plus"></clr-icon>{{'ADD' | translate}}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="clr-col-8" style="text-align: right;">
|
||||||
|
<button id="add" class="btn btn-primary" (click)="gotorunner()">
|
||||||
|
<clr-icon shape="grid-view"></clr-icon>{{'REPORT_RUNNER' | translate}}
|
||||||
|
</button>
|
||||||
|
|
||||||
<clr-datagrid [clrDgLoading]="loading">
|
<button id="add" class="btn btn-primary" (click)="goToAdd()">
|
||||||
|
<clr-icon shape="plus"></clr-icon>{{'ADD' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<clr-datagrid [clrDgLoading]="loading">
|
||||||
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template>
|
<clr-dg-placeholder><ng-template #loadingSpinner><clr-spinner>{{'LOADING' | translate}}</clr-spinner></ng-template>
|
||||||
<div *ngIf="error;else loadingSpinner">{{error}}</div></clr-dg-placeholder>
|
<div *ngIf="error;else loadingSpinner">{{error}}</div>
|
||||||
|
</clr-dg-placeholder>
|
||||||
|
|
||||||
|
|
||||||
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
{{'GO_TO' | translate}}
|
{{'GO_TO' | translate}}
|
||||||
</ng-container></clr-dg-column>
|
</ng-container></clr-dg-column>
|
||||||
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
|
||||||
{{'REPORT_NAME' | translate}}
|
|
||||||
</ng-container></clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
|
||||||
{{'REPORT_DESCRIPTION' | translate}}
|
|
||||||
</ng-container></clr-dg-column>
|
|
||||||
<clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
|
||||||
{{'ACTIVE' | translate}}
|
|
||||||
</ng-container></clr-dg-column>
|
|
||||||
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
|
|
||||||
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
|
|
||||||
</ng-container></clr-dg-column>
|
|
||||||
|
|
||||||
<clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user">
|
<clr-dg-column [clrDgField]="'name'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;" (click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell>
|
{{'REPORT_NAME' | translate}}
|
||||||
<clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell>
|
</ng-container></clr-dg-column>
|
||||||
<clr-dg-cell id="word">{{user.description}}</clr-dg-cell>
|
<clr-dg-column [clrDgField]="'description'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
<clr-dg-cell id="word">{{user.active}}</clr-dg-cell>
|
{{'REPORT_DESCRIPTION' | translate}}
|
||||||
<clr-dg-cell>
|
</ng-container></clr-dg-column>
|
||||||
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true" class="tooltip tooltip-sm tooltip-top-left">
|
|
||||||
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error" style="color: red;"></clr-icon></span>
|
|
||||||
<span class="tooltip-content"> {{'DELETE' | translate}}</span>
|
|
||||||
</a>
|
|
||||||
<clr-signpost>
|
|
||||||
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span>
|
|
||||||
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
|
||||||
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
|
|
||||||
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>
|
|
||||||
<div>{{'CREATED_AT' | translate}}: <code class="clr-code">{{user.createdAt | date}}</code></div>
|
|
||||||
<div>{{'CREATED_BY' | translate}}: <code class="clr-code">{{user.createdBy}}</code></div>
|
|
||||||
<div>{{'UPDATED_AT' | translate}}: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
|
||||||
<div>{{'UPDATED_BY' | translate}}: <code class="clr-code">{{user.updatedBy}}</code></div>
|
|
||||||
</clr-signpost-content>
|
|
||||||
</clr-signpost>
|
|
||||||
|
|
||||||
<!-- <span style="cursor: pointer;"><clr-icon shape="form" (click)="goToLines(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
<clr-dg-column [clrDgField]="''"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
</clr-dg-cell>
|
Sureconnect
|
||||||
<clr-dg-action-overflow>
|
</ng-container></clr-dg-column>
|
||||||
<!-- <button class="action-item" (click)="goToEdit(user.id)">Edit <clr-icon shape="edit" class="is-error"></clr-icon></button> -->
|
|
||||||
</clr-dg-action-overflow>
|
|
||||||
|
|
||||||
<!-- <clr-dg-row-detail *clrIfExpanded >
|
<clr-dg-column [clrDgField]="'active'"><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
|
{{'ACTIVE' | translate}}
|
||||||
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<clr-dg-column><ng-container *clrDgHideableColumn="{hidden: false}">
|
||||||
|
<clr-icon shape="bars"></clr-icon>{{'ACTION' | translate}}
|
||||||
|
</ng-container></clr-dg-column>
|
||||||
|
|
||||||
|
<clr-dg-row *clrDgItems="let user of gridData?.slice()?.reverse();" [clrDgItem]="user">
|
||||||
|
<clr-dg-cell><span class="label label-light-blue" style="display: inline;margin-left: 10px; cursor: pointer;"
|
||||||
|
(click)="goToLines(user)"> {{'SET_UP' | translate}}</span></clr-dg-cell>
|
||||||
|
<clr-dg-cell id="word">{{user.reportName}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell id="word">{{user.description}}</clr-dg-cell>
|
||||||
|
|
||||||
|
<clr-dg-cell id="word">{{user.sureconnect_name}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell id="word">{{user.active}}</clr-dg-cell>
|
||||||
|
<clr-dg-cell>
|
||||||
|
<a href="javascript:void(0)" style="padding-right: 10px;" role="tooltip" aria-haspopup="true"
|
||||||
|
class="tooltip tooltip-sm tooltip-top-left">
|
||||||
|
<span style="cursor: pointer;"><clr-icon shape="trash" (click)="onDelete(user)" class="red is-error"
|
||||||
|
style="color: red;"></clr-icon></span>
|
||||||
|
<span class="tooltip-content"> {{'DELETE' | translate}}</span>
|
||||||
|
</a>
|
||||||
|
<clr-signpost>
|
||||||
|
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
|
||||||
|
style="color: rgb(0, 130, 236);"></clr-icon></span>
|
||||||
|
<clr-signpost-content [clrPosition]="'left-middle'" *clrIfOpen>
|
||||||
|
<h5 style="margin-top: 0">{{'WHO_COLUMN' | translate}}</h5>
|
||||||
|
<div>{{'ACCOUNT_ID' | translate}}: <code class="clr-code">{{user.accountId}}</code></div>
|
||||||
|
<div>{{'CREATED_AT' | translate}}: <code class="clr-code">{{user.createdAt | date}}</code></div>
|
||||||
|
<div>{{'CREATED_BY' | translate}}: <code class="clr-code">{{user.createdBy}}</code></div>
|
||||||
|
<div>{{'UPDATED_AT' | translate}}: <code class="clr-code">{{user.updatedAt | date}}</code></div>
|
||||||
|
<div>{{'UPDATED_BY' | translate}}: <code class="clr-code">{{user.updatedBy}}</code></div>
|
||||||
|
</clr-signpost-content>
|
||||||
|
</clr-signpost>
|
||||||
|
|
||||||
|
<!-- <span style="cursor: pointer;"><clr-icon shape="form" (click)="goToLines(user.id)" class="success" style="color: rgb(0, 130, 236);"></clr-icon></span> -->
|
||||||
|
</clr-dg-cell>
|
||||||
|
<clr-dg-action-overflow>
|
||||||
|
<!-- <button class="action-item" (click)="goToEdit(user.id)">Edit <clr-icon shape="edit" class="is-error"></clr-icon></button> -->
|
||||||
|
</clr-dg-action-overflow>
|
||||||
|
|
||||||
|
<!-- <clr-dg-row-detail *clrIfExpanded >
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-title">actionName: </td>
|
<td class="td-title">actionName: </td>
|
||||||
@@ -77,26 +94,26 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</clr-dg-row-detail> -->
|
</clr-dg-row-detail> -->
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
|
|
||||||
<clr-dg-footer>
|
<clr-dg-footer>
|
||||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{'USERS_PER_PAGE' | translate}}</clr-dg-page-size>
|
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">{{'USERS_PER_PAGE' | translate}}</clr-dg-page-size>
|
||||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||||
of {{pagination.totalItems}} users
|
of {{pagination.totalItems}} users
|
||||||
</clr-dg-pagination>
|
</clr-dg-pagination>
|
||||||
</clr-dg-footer>
|
</clr-dg-footer>
|
||||||
</clr-datagrid>
|
</clr-datagrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<clr-modal [(clrModalOpen)]="modaldelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
<clr-modal [(clrModalOpen)]="modaldelete" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
|
||||||
|
|
||||||
<div class="modal-body" *ngIf="rowSelected.id">
|
<div class="modal-body" *ngIf="rowSelected.id">
|
||||||
<h1 class="delete">{{'DELETE_CONFIRMATION' | translate}}</h1>
|
<h1 class="delete">{{'DELETE_CONFIRMATION' | translate}}</h1>
|
||||||
<h2 class="heading">{{rowSelected.id}}</h2>
|
<h2 class="heading">{{rowSelected.id}}</h2>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline" (click)="modaldelete = false">{{'CANCEL' | translate}}</button>
|
<button type="button" class="btn btn-outline" (click)="modaldelete = false">{{'CANCEL' | translate}}</button>
|
||||||
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary" >{{'DELETE' | translate}}</button>
|
<button type="submit" (click)="delete(rowSelected.id)" class="btn btn-primary">{{'DELETE' | translate}}</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</clr-modal>
|
</div>
|
||||||
|
</clr-modal>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h3 style="font-weight: 300;display: inline;"><b>REPORT SET UP - Project Details Report ({{ReportData.id}})</b></h3>
|
<h3 style="font-weight: 300;display: inline;"><b>REPORT SET UP - Project Details Report ({{ReportData.id}})</b></h3>
|
||||||
<span class="label label-light-blue" style="display: inline;margin-left: 10px;">Edit Mode</span>
|
<span class="label label-light-blue" style="display: inline;margin-left: 10px;">Edit Mode</span>
|
||||||
<hr />
|
<hr />
|
||||||
<form [formGroup]="entryForm">
|
<form [formGroup]="entryForm">
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<div class="clr-col-lg-12 clr-col-md-12 clr-col-sm-12">
|
<div class="clr-col-lg-12 clr-col-md-12 clr-col-sm-12">
|
||||||
<div>
|
<div>
|
||||||
<div class="clr-row">
|
<div class="clr-row">
|
||||||
<!-- <div class="clr-col-md-4 clr-col-sm-12">
|
<!-- <div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<div class="clr-col-sm-12">
|
<div class="clr-col-sm-12">
|
||||||
<label for="projectName">Connection Name</label>
|
<label for="projectName">Connection Name</label>
|
||||||
<select formControlName="conn_name" name="conn_name" [(ngModel)]="nodeEditProperties.conn_name" id="service" class="clr-dropdown">
|
<select formControlName="conn_name" name="conn_name" [(ngModel)]="nodeEditProperties.conn_name" id="service" class="clr-dropdown">
|
||||||
@@ -24,55 +24,70 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="url">Get URL</label>
|
<label for="url">Get URL</label>
|
||||||
<div> <input type="text" class="clr-input" formControlName="url" name="url" [(ngModel)]="nodeEditProperties.url" placeholder="Enter Url" style="width: 76%"> <span><button class="btn btn-icon btn-primary" (click)="getkeys()">
|
<div> <input type="text" class="clr-input" formControlName="url" name="url"
|
||||||
<clr-icon shape="view-list"></clr-icon>
|
[(ngModel)]="nodeEditProperties.url" placeholder="Enter Url" style="width: 76%"> <span><button
|
||||||
</button></span></div>
|
class="btn btn-icon btn-primary" (click)="getkeys()">
|
||||||
</div>
|
<clr-icon shape="view-list"></clr-icon>
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
</button></span></div>
|
||||||
<label for="workflow_name">Include Date filter</label>
|
</div>
|
||||||
<input type="checkbox" formControlName="date_param_req" name="date_param_req" [(ngModel)]="nodeEditProperties.date_param_req" clrToggle />
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
</div>
|
<label for="workflow_name">Include Date filter</label>
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<input type="checkbox" formControlName="date_param_req" name="date_param_req"
|
||||||
<label>Standard Parameters</label>
|
[(ngModel)]="nodeEditProperties.date_param_req" clrToggle />
|
||||||
<clr-combobox-container style="margin-top: 0; padding-top: 0;">
|
</div>
|
||||||
<!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> -->
|
|
||||||
<clr-combobox formControlName="std_param_html" name="std_param_html" [(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true"
|
|
||||||
required>
|
|
||||||
<ng-container *clrOptionSelected="let selected">
|
|
||||||
{{selected}}
|
|
||||||
</ng-container>
|
|
||||||
<clr-options>
|
|
||||||
<clr-option *clrOptionItems="let state of keysfromurl" [clrValue]="state">
|
|
||||||
{{state}}
|
|
||||||
</clr-option>
|
|
||||||
</clr-options>
|
|
||||||
</clr-combobox>
|
|
||||||
</clr-combobox-container>
|
|
||||||
|
|
||||||
</div>
|
<!-- SureConnect Dropdown -->
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label>List</label>
|
<label for="sureConnectId">SureConnect Connection</label>
|
||||||
<select>
|
<select formControlName="sureConnectId" class="clr-select">
|
||||||
<option value="">Choose from list</option>
|
<option value="">-- Select SureConnect --</option>
|
||||||
<option></option>
|
<option *ngFor="let conn of sureconnectData" [value]="conn.id">
|
||||||
</select>
|
{{conn.name || conn.id}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<!-- <div class="clr-col-md-4 clr-col-sm-12">
|
<label>Standard Parameters</label>
|
||||||
|
<clr-combobox-container style="margin-top: 0; padding-top: 0;">
|
||||||
|
<!-- <label style="padding-bottom: 5px; padding-top:0px; font-weight: lighter;" class="p1">Select Left Side Filter</label> -->
|
||||||
|
<clr-combobox formControlName="std_param_html" name="std_param_html"
|
||||||
|
[(ngModel)]="nodeEditProperties.std_param_html" clrMulti="true" required>
|
||||||
|
<ng-container *clrOptionSelected="let selected">
|
||||||
|
{{selected}}
|
||||||
|
</ng-container>
|
||||||
|
<clr-options>
|
||||||
|
<clr-option *clrOptionItems="let state of keysfromurl" [clrValue]="state">
|
||||||
|
{{state}}
|
||||||
|
</clr-option>
|
||||||
|
</clr-options>
|
||||||
|
</clr-combobox>
|
||||||
|
</clr-combobox-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
|
<label>List</label>
|
||||||
|
<select>
|
||||||
|
<option value="">Choose from list</option>
|
||||||
|
<option></option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- <div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<div class="clr-col-sm-12">
|
<div class="clr-col-sm-12">
|
||||||
<label for="description" class="d1">Adhoc Parameter String (html)</label>
|
<label for="description" class="d1">Adhoc Parameter String (html)</label>
|
||||||
<textarea id="t1" cols="10" rows="3" formControlName="adhoc_param_html" placeholder="Enter Description" name="adhoc_param_html" [(ngModel)]="nodeEditProperties.adhoc_param_html" style="width:100%">
|
<textarea id="t1" cols="10" rows="3" formControlName="adhoc_param_html" placeholder="Enter Description" name="adhoc_param_html" [(ngModel)]="nodeEditProperties.adhoc_param_html" style="width:100%">
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<br>
|
</div>
|
||||||
<!-- <div class="clr-row" style="padding-left:10px;">
|
</div>
|
||||||
|
<br>
|
||||||
|
<!-- <div class="clr-row" style="padding-left:10px;">
|
||||||
<div class="clr-col-md-4 clr-col-sm-12">
|
<div class="clr-col-md-4 clr-col-sm-12">
|
||||||
<label for="description" class="d1">Column String (html)</label>
|
<label for="description" class="d1">Column String (html)</label>
|
||||||
<textarea id="t1" cols="10" rows="3" formControlName="column_str" placeholder="Enter Description" name="column_str" [(ngModel)]="nodeEditProperties.column_str" style="width:100%">
|
<textarea id="t1" cols="10" rows="3" formControlName="column_str" placeholder="Enter Description" name="column_str" [(ngModel)]="nodeEditProperties.column_str" style="width:100%">
|
||||||
@@ -85,11 +100,10 @@
|
|||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
<br>
|
<br>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<button type="button" class="btn btn-outline" (click)="back()">BACK</button>
|
<button type="button" class="btn btn-outline" (click)="back()">BACK</button>
|
||||||
<button type="submit" form-control class="btn btn-primary" (click)="onSubmit()">UPDATE</button>
|
<button type="submit" form-control class="btn btn-primary" (click)="onSubmit()">UPDATE</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
import { ReportBuilderService } from 'src/app/services/api/report-builder.service';
|
||||||
|
import { SureconnectService } from '../../dashboardnew/sureconnect/sureconnect.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -23,11 +24,17 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
date_param_req: '',
|
date_param_req: '',
|
||||||
// folderName:'',
|
// folderName:'',
|
||||||
url: '',
|
url: '',
|
||||||
|
// Add sureConnectId property
|
||||||
|
sureConnectId: null,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
// Add sureconnect data property
|
||||||
|
sureconnectData: any[] = [];
|
||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
|
private route: ActivatedRoute, private reportBuilderService: ReportBuilderService,
|
||||||
private toastr: ToastrService, private _fb: FormBuilder) { }
|
private toastr: ToastrService, private _fb: FormBuilder,
|
||||||
|
private sureconnectService: SureconnectService) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.id = this.route.snapshot.params["id"];
|
this.id = this.route.snapshot.params["id"];
|
||||||
@@ -41,11 +48,26 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
date_param_req: [null],
|
date_param_req: [null],
|
||||||
// folderName:[null],
|
// folderName:[null],
|
||||||
url: [null],
|
url: [null],
|
||||||
|
// Add sureConnectId to form
|
||||||
|
sureConnectId: [null],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load sureconnect data first, then load report data
|
||||||
|
this.loadSureconnectData();
|
||||||
this.getById(this.id);
|
this.getById(this.id);
|
||||||
this.listoddatabase();
|
this.listoddatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add method to load sureconnect data
|
||||||
|
loadSureconnectData() {
|
||||||
|
this.sureconnectService.getAll().subscribe((data: any[]) => {
|
||||||
|
this.sureconnectData = data;
|
||||||
|
console.log('Sureconnect data loaded:', this.sureconnectData);
|
||||||
|
}, (error) => {
|
||||||
|
console.log('Error loading sureconnect data:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
databaselist;
|
databaselist;
|
||||||
listoddatabase() {
|
listoddatabase() {
|
||||||
this.reportBuilderService.getdatabse().subscribe((data) => {
|
this.reportBuilderService.getdatabse().subscribe((data) => {
|
||||||
@@ -81,6 +103,11 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
|
// this.nodeEditProperties.conn_name = this.builderLineData.conn_name;
|
||||||
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
|
this.nodeEditProperties.date_param_req = this.builderLineData[0].date_param_req;
|
||||||
this.nodeEditProperties.url = this.builderLineData[0].url;
|
this.nodeEditProperties.url = this.builderLineData[0].url;
|
||||||
|
// Set sureConnectId if it exists in the data
|
||||||
|
this.nodeEditProperties.sureConnectId = this.builderLineData[0].sureConnectId || null;
|
||||||
|
|
||||||
|
// Update form with loaded data
|
||||||
|
this.entryForm.patchValue(this.nodeEditProperties);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
@@ -115,6 +142,8 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
|
adhoc_param_html: this.nodeEditProperties.adhoc_param_html,
|
||||||
date_param_req: this.nodeEditProperties.date_param_req,
|
date_param_req: this.nodeEditProperties.date_param_req,
|
||||||
url: this.nodeEditProperties.url,
|
url: this.nodeEditProperties.url,
|
||||||
|
// Add sureConnectId to the data
|
||||||
|
sureConnectId: this.nodeEditProperties.sureConnectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
|
this.builderLineData[0].std_param_html = this.nodeEditProperties.std_param_html;
|
||||||
@@ -123,6 +152,9 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
|
// this.builderLineData.conn_name = this.nodeEditProperties.conn_name ;
|
||||||
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
|
this.builderLineData[0].date_param_req = this.nodeEditProperties.date_param_req;
|
||||||
this.builderLineData[0].url = this.nodeEditProperties.url;
|
this.builderLineData[0].url = this.nodeEditProperties.url;
|
||||||
|
// Add sureConnectId to the data
|
||||||
|
this.builderLineData[0].sureConnectId = this.nodeEditProperties.sureConnectId;
|
||||||
|
|
||||||
console.log(this.builderLineData);
|
console.log(this.builderLineData);
|
||||||
// this.builderLineData.splice(1);
|
// this.builderLineData.splice(1);
|
||||||
console.log(this.builderLineData);
|
console.log(this.builderLineData);
|
||||||
@@ -148,6 +180,8 @@ export class ReportBuild2editComponent implements OnInit {
|
|||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.updated = true;
|
this.updated = true;
|
||||||
|
// Update nodeEditProperties with form values including sureConnectId
|
||||||
|
Object.assign(this.nodeEditProperties, this.entryForm.value);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,16 @@ import { ExcelService } from 'src/app/services/excel.service';
|
|||||||
})
|
})
|
||||||
export class Reportrunneredit2Component implements OnInit {
|
export class Reportrunneredit2Component implements OnInit {
|
||||||
dynamicForm: FormGroup;
|
dynamicForm: FormGroup;
|
||||||
modalselect: boolean = false;
|
modalselect:boolean=false;
|
||||||
serverData = [{
|
serverData = [{"andor": "AND",
|
||||||
"andor": "AND",
|
"fields_name": "",
|
||||||
"fields_name": "",
|
"condition": "=",
|
||||||
"condition": "=",
|
"value": ""}];
|
||||||
"value": ""
|
andor = ['AND', 'OR','NOT'];
|
||||||
}];
|
|
||||||
andor = ['AND', 'OR', 'NOT'];
|
|
||||||
fieldname = ['name1', 'name2'];
|
fieldname = ['name1', 'name2'];
|
||||||
condition = ['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'BETWEEN', 'IN'];
|
condition = ['=','!=','<','>','<=','>=','LIKE','BETWEEN','IN'];
|
||||||
header_id;
|
header_id;
|
||||||
public array = [
|
public array=[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jack",
|
"name": "Jack",
|
||||||
@@ -78,12 +76,13 @@ export class Reportrunneredit2Component implements OnInit {
|
|||||||
selectedfrom;
|
selectedfrom;
|
||||||
selectedto;
|
selectedto;
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute, private _fb: FormBuilder,
|
private route: ActivatedRoute,private _fb: FormBuilder,
|
||||||
private reportBuilderService: ReportBuilderService, private toastr: ToastrService, private sanitizer: DomSanitizer, private excel: ExcelService) {
|
private reportBuilderService: ReportBuilderService,private toastr:ToastrService,private sanitizer: DomSanitizer,private excel: ExcelService)
|
||||||
this.dynamicForm = this._fb.group({
|
{
|
||||||
});
|
this.dynamicForm = this._fb.group({
|
||||||
}
|
});
|
||||||
todayDate;
|
}
|
||||||
|
todayDate;
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.todayDate = new Date().toISOString().slice(0, 10);
|
this.todayDate = new Date().toISOString().slice(0, 10);
|
||||||
this.header_id = this.route.snapshot.params["id"];
|
this.header_id = this.route.snapshot.params["id"];
|
||||||
@@ -102,11 +101,14 @@ export class Reportrunneredit2Component implements OnInit {
|
|||||||
builderLine;
|
builderLine;
|
||||||
builderLineData;
|
builderLineData;
|
||||||
lineId;
|
lineId;
|
||||||
adhocList: any[];
|
adhocList:any[];
|
||||||
SQLQuery;
|
SQLQuery;
|
||||||
getUrl;
|
getUrl;
|
||||||
|
// Add sureConnectId property
|
||||||
|
sureConnectId: number | null = null;
|
||||||
stdParamfields;
|
stdParamfields;
|
||||||
DateParam;
|
DateParam;
|
||||||
|
|
||||||
getById(id: number) {
|
getById(id: number) {
|
||||||
this.reportBuilderService.getrbDetailsById(id).subscribe(
|
this.reportBuilderService.getrbDetailsById(id).subscribe(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -114,31 +116,44 @@ export class Reportrunneredit2Component implements OnInit {
|
|||||||
this.reportName = data.reportName;
|
this.reportName = data.reportName;
|
||||||
this.builderLine = data.rpt_builder2_lines;
|
this.builderLine = data.rpt_builder2_lines;
|
||||||
this.lineId = this.builderLine[0].id
|
this.lineId = this.builderLine[0].id
|
||||||
this.builderLineData = JSON.parse(this.builderLine[0].model);
|
this.builderLineData = JSON.parse(this.builderLine[0].model) ;
|
||||||
console.log(this.lineId, this.builderLineData);
|
console.log(this.lineId,this.builderLineData);
|
||||||
this.builderLineData = this.builderLineData[0];
|
this.builderLineData = this.builderLineData[0];
|
||||||
this.adhocList = this.builderLineData.adhoc_param_html;
|
this.adhocList = this.builderLineData.adhoc_param_html;
|
||||||
// this.adhocList = JSON.parse(adhocList);
|
// this.adhocList = JSON.parse(adhocList);
|
||||||
this.DateParam = this.builderLineData.date_param_req;
|
this.DateParam = this.builderLineData.date_param_req;
|
||||||
this.getUrl = this.builderLineData.url;
|
this.getUrl = this.builderLineData.url;
|
||||||
console.log(this.adhocList, this.DateParam, this.getUrl)
|
// Get sureConnectId if it exists
|
||||||
|
this.sureConnectId = this.builderLineData.sureConnectId || null;
|
||||||
|
console.log(this.adhocList,this.DateParam,this.getUrl)
|
||||||
this.getStdParam(this.header_id);
|
this.getStdParam(this.header_id);
|
||||||
this.featchData();
|
this.featchData();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
featchData() {
|
/**
|
||||||
this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data => {
|
* Fetch data using the URL and SureConnect connection if available
|
||||||
|
*/
|
||||||
|
featchData(){
|
||||||
|
// If sureConnectId is available, we might want to modify the request
|
||||||
|
// For now, we'll use the existing implementation but with better error handling
|
||||||
|
this.reportBuilderService.getAllDetailsByurl(this.getUrl).subscribe(data =>{
|
||||||
console.log(data);
|
console.log(data);
|
||||||
if (data.body) {
|
if(data.body){
|
||||||
// Check if the response is XML (starts with <) or JSON
|
// Check if the response is XML (starts with <) or JSON
|
||||||
if (typeof data.body === 'string' && data.body.trim().startsWith('<')) {
|
if (typeof data.body === 'string' && data.body.trim().startsWith('<')) {
|
||||||
// Handle XML response - for now we'll set rows to empty array
|
// Handle XML response
|
||||||
// In a real implementation, you would parse the XML properly
|
console.log('Received XML response, parsing to JSON');
|
||||||
console.warn('Received XML response instead of JSON');
|
try {
|
||||||
this.rows = [];
|
this.rows = this.parseXMLToJSON(data.body);
|
||||||
this.filterRows = [];
|
this.filterRows = [...this.rows]; // Create a copy
|
||||||
|
console.log('Parsed XML data:', this.rows);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing XML:', error);
|
||||||
|
this.rows = [];
|
||||||
|
this.filterRows = [];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle JSON response
|
// Handle JSON response
|
||||||
try {
|
try {
|
||||||
@@ -858,4 +873,82 @@ export class Reportrunneredit2Component implements OnInit {
|
|||||||
value.dayOfMonth !== undefined)
|
value.dayOfMonth !== undefined)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple XML parser to convert XML to JSON array
|
||||||
|
* Handles the specific format: <User><id>101</id><name>John Doe</name><email>john.doe@example.com</email></User>
|
||||||
|
* @param xmlString The XML string to parse
|
||||||
|
* @returns Array of objects representing the XML data
|
||||||
|
*/
|
||||||
|
private parseXMLToJSON(xmlString: string): any[] {
|
||||||
|
// Remove any XML declaration
|
||||||
|
xmlString = xmlString.replace(/<\?xml[^>]*\?>/g, '');
|
||||||
|
|
||||||
|
// Extract the root element name (e.g., "User" from <User>...</User>)
|
||||||
|
const rootMatch = xmlString.match(/<(\w+)(?:\s[^>]*)?>/);
|
||||||
|
if (!rootMatch) {
|
||||||
|
console.warn('Could not identify root element in XML');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootElement = rootMatch[1];
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
|
// Create a regex to match all instances of the root element
|
||||||
|
const regex = new RegExp(`<${rootElement}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${rootElement}>`, 'g');
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(xmlString)) !== null) {
|
||||||
|
const elementContent = match[1];
|
||||||
|
const item: any = {};
|
||||||
|
|
||||||
|
// Extract all tags and their values
|
||||||
|
const tagRegex = /<(\w+)>(.*?)<\/\1>/g;
|
||||||
|
let tagMatch;
|
||||||
|
|
||||||
|
while ((tagMatch = tagRegex.exec(elementContent)) !== null) {
|
||||||
|
const tagName = tagMatch[1];
|
||||||
|
const tagValue = tagMatch[2];
|
||||||
|
// Try to convert to appropriate type
|
||||||
|
item[tagName] = this.convertValueType(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert string values to appropriate types (number, boolean, etc.)
|
||||||
|
* @param value The string value to convert
|
||||||
|
* @returns The value converted to the appropriate type
|
||||||
|
*/
|
||||||
|
private convertValueType(value: string): any {
|
||||||
|
// Check for empty value
|
||||||
|
if (value === '') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for boolean values
|
||||||
|
if (value.toLowerCase() === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (value.toLowerCase() === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for numeric values
|
||||||
|
if (!isNaN(Number(value)) && !isNaN(parseFloat(value))) {
|
||||||
|
// Check if it's an integer or float
|
||||||
|
if (Number.isInteger(parseFloat(value))) {
|
||||||
|
return parseInt(value, 10);
|
||||||
|
} else {
|
||||||
|
return parseFloat(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as string if no other type matches
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -126,6 +126,7 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
|
|||||||
import { QueryComponent } from './superadmin/query/query.component';
|
import { QueryComponent } from './superadmin/query/query.component';
|
||||||
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
import { QueryaddComponent } from './superadmin/queryadd/queryadd.component';
|
||||||
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
import { QueryeditComponent } from './superadmin/queryedit/queryedit.component';
|
||||||
|
import { ChartTypePageComponent } from './builder/dashboardnew/chart-type-manager/chart-type-page.component';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -222,6 +223,7 @@ const routes: Routes = [
|
|||||||
{ path: 'editdata/:id', component: EditformnewdashComponent },
|
{ path: 'editdata/:id', component: EditformnewdashComponent },
|
||||||
{ path: 'editdashn/:id', component: EditnewdashComponent },
|
{ path: 'editdashn/:id', component: EditnewdashComponent },
|
||||||
{ path: 'schedule/:id', component: ScheduleComponent },
|
{ path: 'schedule/:id', component: ScheduleComponent },
|
||||||
|
{ path: 'chart-types', component: ChartTypePageComponent },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,14 @@ import { OauthComponent } from './builder/dashboardnew/sureconnect/oauth/oauth.c
|
|||||||
|
|
||||||
// Import Shield Dashboard Module
|
// Import Shield Dashboard Module
|
||||||
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
|
import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-dashboard/shield-dashboard.module';
|
||||||
|
// Import UnifiedChartComponent
|
||||||
|
import { UnifiedChartComponent } from './builder/dashboardnew/gadgets/unified-chart/unified-chart.component';
|
||||||
|
// Import ChartConfigManagerComponent
|
||||||
|
import { ChartConfigManagerComponent } from './builder/dashboardnew/chart-config-manager/chart-config-manager.component';
|
||||||
|
// Import ChartTypeManagerComponent
|
||||||
|
import { ChartTypeManagerComponent } from './builder/dashboardnew/chart-type-manager/chart-type-manager.component';
|
||||||
|
// Import ChartTypePageComponent
|
||||||
|
import { ChartTypePageComponent } from './builder/dashboardnew/chart-type-manager/chart-type-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -173,6 +181,14 @@ import { ShieldDashboardModule } from './builder/dashboardnew/gadgets/shield-das
|
|||||||
SubmenuComponent, ModulesComponent, SessionloggerComponent,
|
SubmenuComponent, ModulesComponent, SessionloggerComponent,
|
||||||
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
|
DashboardnewComponent, EditformnewdashComponent, EditnewdashComponent, ScheduleComponent,
|
||||||
CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
|
CommonFilterComponent, ChartWrapperComponent, CompactFilterComponent, DoughnutChartComponent, LineChartComponent, RadarChartComponent, BarChartComponent, BubbleChartComponent, DynamicChartComponent, ScatterChartComponent, PolarChartComponent, PieChartComponent, FinancialChartComponent, ToDoChartComponent, GridViewComponent,
|
||||||
|
// Add UnifiedChartComponent to declarations
|
||||||
|
UnifiedChartComponent,
|
||||||
|
// Add ChartConfigManagerComponent to declarations
|
||||||
|
ChartConfigManagerComponent,
|
||||||
|
// Add ChartTypeManagerComponent to declarations
|
||||||
|
ChartTypeManagerComponent,
|
||||||
|
// Add ChartTypePageComponent to declarations
|
||||||
|
ChartTypePageComponent,
|
||||||
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
|
DashrunnerlineComponent, BarRunnerComponent, LineRunnerComponent, DoughnutRunnerComponent, GridRunnerComponent, PieRunnerComponent, PolarRunnerComponent, RadarRunnerComponent, ScatterRunnerComponent, TodoRunnerComponent, BubbleRunnerComponent,
|
||||||
// Add CompactFilterRunnerComponent to declarations
|
// Add CompactFilterRunnerComponent to declarations
|
||||||
CompactFilterRunnerComponent,
|
CompactFilterRunnerComponent,
|
||||||
|
|||||||
@@ -69,4 +69,13 @@ export class AlertsService {
|
|||||||
}
|
}
|
||||||
return this.apiRequest.get(apiUrl);
|
return this.apiRequest.get(apiUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get values for a specific key from API
|
||||||
|
public getValuesFromUrl(url: string, sureId: number | undefined, key: string): Observable<any> {
|
||||||
|
let apiUrl = `chart/getValue?apiUrl=${url}&key=${key}`;
|
||||||
|
if (sureId) {
|
||||||
|
apiUrl += `&sureId=${sureId}`;
|
||||||
|
}
|
||||||
|
return this.apiRequest.get(apiUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user