Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.html	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.html	(working copy)
@@ -14,12 +14,19 @@
         </div>
       </ng-template>
     </mat-tab>
-    <mat-tab label="Customized Files">
+    <mat-tab label="Custom Configuration">
       <ng-template matTabContent>
         <div class="tab-content">
           <app-device-custom-config-overview/>
         </div>
       </ng-template>
     </mat-tab>
+    <mat-tab label="Custom Templates">
+      <ng-template matTabContent>
+        <div class="tab-content">
+          <app-device-custom-config-templates/>
+        </div>
+      </ng-template>
+    </mat-tab>
   </mat-tab-group>
 </div>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/config-hub/config-hub.ts	(working copy)
@@ -7,10 +7,13 @@
 } from '../sub-components/device-custom-config-overview/device-custom-config-overview';
 import {DeviceClonedFiles} from '../sub-components/device-cloned-files/device-cloned-files';
 import {DeviceConfigOverview} from '../sub-components/device-config-overview/device-config-overview';
+import {
+  DeviceCustomConfigTemplates
+} from '../sub-components/device-custom-config-templates/device-custom-config-templates';
 
 @Component({
   selector: 'app-config-hub',
-  imports: [SharedModule, DeviceCustomConfigOverview, DeviceClonedFiles, DeviceConfigOverview],
+  imports: [SharedModule, DeviceCustomConfigOverview, DeviceClonedFiles, DeviceConfigOverview, DeviceCustomConfigTemplates],
   templateUrl: './config-hub.html',
   styleUrl: './config-hub.scss'
 })
@@ -20,7 +23,8 @@
   private tabNames: string[] = [
     'Device Configuration Files',
     'Cloned Files',
-    'Customized Files'
+    'Custom Configuration',
+    'Custom Templates',
   ];
 
   constructor(private route: ActivatedRoute, private router: Router) {
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/device-cloned-files.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/device-cloned-files.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/device-cloned-files.ts	(working copy)
@@ -51,6 +51,7 @@
     setTimeout(() => {
       this.getDeviceConfigFiles();
       this.getAMPDevicesList();
+      this.getDeviceGroups();
     })
   }
 
@@ -105,6 +106,39 @@
         this._cdRef.detectChanges();
       }
     })
+  }
+
+  getDeviceGroups() {
+    this.groups = [];
+    this.deviceGroups = []
+    // ToDo: Update with actual RoleId
+    let roleId = "0"
+    let rawPayload = new FormData();
+    rawPayload.set('action', 'FilterRoleDeviceGroups');
+    rawPayload.set('options', JSON.stringify({"role_id": roleId}));
+    this._device.getDeviceGroups(rawPayload)
+      .pipe(take(1))
+      .subscribe({
+        next: (result: any) => {
+          if (result && result.length > 1) {
+            if (result[1] && 'result' in result[1]) {
+              let groups = result[1].result;
+              groups.forEach((group: any) => {
+                if (group?.device_list.length > 0) {
+                  this.groups.push(group?.group_name);
+                  this.deviceGroups.push(group);
+                }
+                // group?.device_list.forEach((device: any) => {
+                //   this.devices.push(device);
+                // })
+              })
+            }
+          }
+        }, error: (error: { message: string; }) => {
+          console.log(error);
+          this._notification.showError(error.message);
+        }
+      })
   }
 
   getGlobalSerial(index: number): number {
@@ -271,7 +305,7 @@
       })],
       module: this.configForm.value.module,
       option: this.configForm.value.option,
-      device: this.data?.device?.name,
+      device: this.configForm.value.device,
     }
     if (this.configForm.value.module === 'custom') {
       rawPayload['custom'] = this.configForm.value.custom;
@@ -506,7 +540,9 @@
       if (this.data?.config?.device_type.toLowerCase() === 'vapv' ||
         this.data?.config?.device_type.toLowerCase() === 'apv' ||
         this.data?.config?.device_type.toLowerCase() === 'vasf' ||
-        this.data?.config?.device_type.toLowerCase() === 'asf') {
+        this.data?.config?.device_type.toLowerCase() === 'asf' ||
+        this.data?.config?.file_type.toLowerCase() === 'customize' ||
+        this.data?.config?.file_type.toLowerCase() === 'template') {
         this.getConfig();
       } else if (this.data?.config?.device_type.toLowerCase() === 'vxag' || this.data?.config?.device_type.toLowerCase() === 'ag') {
         this.getAGConfigAssociatedVsites();
@@ -521,7 +557,7 @@
   }
 
   getConfig() {
-    this._device.getDeviceConfigByConfigFileName(this.data?.config?.name)
+    this._device.getDeviceConfigByConfigFileName(this.data?.config?.name, this.data?.config?.file_type)
       .pipe(take(1))
       .subscribe({
         next: (result: any) => {
@@ -598,7 +634,7 @@
     payload.set('data', JSON.stringify({
       config_content: this.configForm.value.device_config,
     }));
-    if (this.deviceType === 'ag' || this.deviceType === 'vxag') {
+    if ((this.deviceType === 'ag' || this.deviceType === 'vxag') && this.data?.config?.file_type.toLowerCase() !== 'customize' && this.data?.config?.file_type.toLowerCase() !== 'template') {
       filename = filename.split('.')[0];
       this._device.updateAGVSiteConfig(filename, this.configForm.value.aGOption, payload)
         .pipe(take(1)).subscribe({
@@ -615,7 +651,7 @@
         }
       })
     } else {
-      this._device.updateDeviceConfigByConfigFileName(filename, payload)
+      this._device.updateDeviceConfigByConfigFileName(filename, this.data?.config?.file_type, payload)
         .pipe(take(1)).subscribe({
         next: (result: any) => {
           if (result) {
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/edit-configuration.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/edit-configuration.html	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-cloned-files/edit-configuration.html	(working copy)
@@ -4,7 +4,7 @@
     (ngSubmit)="onSubmit()"
     [formGroup]="configForm"
   >
-    @if (deviceType === 'ag' || deviceType === 'vxag') {
+    @if ((deviceType === 'ag' || deviceType === 'vxag') && this.data?.config?.file_type !== 'customize' && this.data?.config?.file_type !== 'template') {
       <div class="form-field-wrapper">
         <label for="aGOption" class="form-label">Select Virtual Site *</label>
         <mat-form-field appearance="outline" subscriptSizing="dynamic">
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/create-custom-config.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/create-custom-config.html	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/create-custom-config.html	(working copy)
@@ -0,0 +1,344 @@
+<h2 mat-dialog-title>Create Custom Configuration</h2>
+<mat-dialog-content>
+  <form
+    (ngSubmit)="onSubmit()"
+    [formGroup]="configForm"
+  >
+    <div class="form-field-wrapper">
+      <label for="config_type" class="form-label">Configuration Type *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <mat-select formControlName="config_type">
+          @for (_option of configOption; track _option) {
+            <mat-option [value]="_option.value">{{ _option.label }}</mat-option>
+          }
+        </mat-select>
+        @if (configForm.get('config_type')?.invalid && configForm.get('config_type')?.touched) {
+          <mat-error>
+            @if (configForm.get('config_type')?.errors?.['required']) {
+              Configuration type is required.
+            } @else {
+              Invalid configuration type format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="filename" class="form-label">Filename *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="filename"
+          formControlName="filename"
+          matInput
+          placeholder="Filename"
+          type="text"
+        />
+        @if (configForm.get('filename')?.invalid && configForm.get('filename')?.touched) {
+          <mat-error>
+            @if (configForm.get('filename')?.errors?.['required']) {
+              Filename is required.
+            } @else {
+              Invalid filename format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="device_type" class="form-label">Device Type *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <mat-select formControlName="device_type">
+          @for (_option of deviceTypes; track _option) {
+            <mat-option [value]="_option">{{ _option }}</mat-option>
+          }
+        </mat-select>
+        @if (configForm.get('device_type')?.invalid && configForm.get('device_type')?.touched) {
+          <mat-error>
+            @if (configForm.get('device_type')?.errors?.['required']) {
+              Device type is required.
+            } @else {
+              Invalid device type format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    @if (configForm.value?.config_type === 'file') {
+      <div class="form-field-wrapper">
+        <label for="fill_type" class="form-label">Upload File *</label>
+        <input formControlName="config_file_upload" type="file" (change)="onFileUploadToServer($event)">
+        <mat-hint>
+
+        </mat-hint>
+        @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+          <mat-error>
+            @if (configForm.get('config_content')?.errors?.['required']) {
+              Device configuration is required.
+            } @else {
+              Invalid device configuration format.
+            }
+          </mat-error>
+        }
+      </div>
+      <p>
+        Please use ##URL## to represent the URL used for the import. ##URL## will be replaced by the URL of the uploaded
+        local file on a FTP server
+      </p>
+    }
+    @if (configForm.value.config_type === 'file') {
+      @if (configForm.value.device_type.toLowerCase() !== 'ag' && configForm.value.device_type.toLowerCase() !== 'vxag') {
+        <div class="form-field-wrapper">
+          <label for="using" class="form-label">Using *</label>
+          <mat-radio-group formControlName="using" (change)="onFillTypeChange()">
+            <mat-radio-button value="blank">Use Blank Content</mat-radio-button>
+            <mat-radio-button value="template">Use Template</mat-radio-button>
+          </mat-radio-group>
+        </div>
+        @if (configForm.value.using === 'template') {
+          <div class="form-field-wrapper">
+            <label for="template_type" class="form-label">Template Type *</label>
+            <mat-radio-group formControlName="template_type" (change)="onTemplateChange()">
+              <mat-radio-button value="import">Import And Check Virtual Host Certificate Template</mat-radio-button>
+              <mat-radio-button value="vhost">Check Virtual Host Certificate Template</mat-radio-button>
+            </mat-radio-group>
+          </div>
+          <p>Template Keywords</p>
+          <div class="form-field-wrapper">
+            <label for="hostname" class="form-label">HostName *</label>
+            <mat-form-field appearance="outline" subscriptSizing="dynamic">
+              <input
+                id="hostname"
+                formControlName="hostname"
+                matInput
+                placeholder="HostName"
+                type="text"
+              />
+              @if (configForm.get('hostname')?.invalid && configForm.get('hostname')?.touched) {
+                <mat-error>
+                  @if (configForm.get('hostname')?.errors?.['required']) {
+                    Hostname is required.
+                  } @else {
+                    Invalid hostname format.
+                  }
+                </mat-error>
+              }
+            </mat-form-field>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="cert_index" class="form-label">Cert Index *</label>
+            <mat-form-field appearance="outline" subscriptSizing="dynamic">
+              <input
+                id="cert_index"
+                formControlName="cert_index"
+                matInput
+                placeholder="CertIndex"
+                type="text"
+              />
+              @if (configForm.get('cert_index')?.invalid && configForm.get('cert_index')?.touched) {
+                <mat-error>
+                  @if (configForm.get('cert_index')?.errors?.['required']) {
+                    CertIndex is required.
+                  } @else {
+                    Invalid Cert Index format.
+                  }
+                </mat-error>
+              }
+            </mat-form-field>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="cert_type" class="form-label">Cert Type *</label>
+            <mat-form-field appearance="outline" subscriptSizing="dynamic">
+              <input
+                id="cert_type"
+                formControlName="cert_type"
+                matInput
+                placeholder="CertType"
+                type="text"
+              />
+              @if (configForm.get('cert_type')?.invalid && configForm.get('cert_type')?.touched) {
+                <mat-error>
+                  @if (configForm.get('cert_type')?.errors?.['required']) {
+                    CertType is required.
+                  } @else {
+                    Invalid Cert Type format.
+                  }
+                </mat-error>
+              }
+            </mat-form-field>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="servername" class="form-label">Indication *</label>
+            <mat-form-field appearance="outline" subscriptSizing="dynamic">
+              <input
+                id="servername"
+                formControlName="servername"
+                matInput
+                placeholder="ServerName Indication"
+                type="text"
+              />
+              @if (configForm.get('servername')?.invalid && configForm.get('servername')?.touched) {
+                <mat-error>
+                  @if (configForm.get('servername')?.errors?.['required']) {
+                    ServerName Indification is required.
+                  } @else {
+                    Invalid ServerName Indification format.
+                  }
+                </mat-error>
+              }
+            </mat-form-field>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="password" class="form-label">Password *</label>
+            <mat-form-field appearance="outline" subscriptSizing="dynamic">
+              <input
+                id="password"
+                formControlName="password"
+                matInput
+                placeholder="Password"
+                type="text"
+              />
+              @if (configForm.get('password')?.invalid && configForm.get('password')?.touched) {
+                <mat-error>
+                  @if (configForm.get('password')?.errors?.['required']) {
+                    Password is required.
+                  } @else {
+                    Invalid Password format.
+                  }
+                </mat-error>
+              }
+            </mat-form-field>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="template_content" class="form-label">Template Content *</label>
+            <textarea matInput formControlName="template_content" cols="40" rows="5"></textarea>
+          </div>
+          <div class="form-field-wrapper">
+            <label for="config_preview" class="form-label">Config Preview *</label>
+            <textarea matInput formControlName="config_preview" cols="40" rows="5"></textarea>
+          </div>
+        } @else {
+          <div class="form-field-wrapper">
+            <label for="fill_type" class="form-label">Config Content *</label>
+            <textarea matInput formControlName="config_content" cols="40" rows="5"></textarea>
+            @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+              <mat-error>
+                @if (configForm.get('config_content')?.errors?.['required']) {
+                  Device configuration is required.
+                } @else {
+                  Invalid device configuration format.
+                }
+              </mat-error>
+            }
+          </div>
+        }
+      } @else if (configForm.value.device_type.toLowerCase() === 'ag' || configForm.value.device_type.toLowerCase() === 'vxag') {
+        <div class="form-field-wrapper">
+          <label for="fill_type" class="form-label">Config Content *</label>
+          <textarea matInput formControlName="config_content" cols="40" rows="5"></textarea>
+          @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+            <mat-error>
+              @if (configForm.get('config_content')?.errors?.['required']) {
+                Device configuration is required.
+              } @else {
+                Invalid device configuration format.
+              }
+            </mat-error>
+          }
+        </div>
+      }
+    }
+    @if (configForm.value?.config_type === 'text') {
+      <div class="form-field-wrapper">
+        <label for="fill_type" class="form-label">Fill Type *</label>
+        <mat-radio-group formControlName="fill_type" (change)="onFillTypeChange()">
+          <mat-radio-button value="text">Text</mat-radio-button>
+          <mat-radio-button value="file">File</mat-radio-button>
+        </mat-radio-group>
+      </div>
+    }
+    @if (configForm.value?.config_type === 'template') {
+      <p>
+        Please use %%template_key%% to represent the key used for the template configuration. %%template_key%% will be
+        found and replaced by the value that maintain in Template Files when applying to devices.
+      </p>
+      <div class="form-field-wrapper">
+        <label for="fill_type" class="form-label">Config Content *</label>
+        <textarea matInput formControlName="config_content" cols="40" rows="5"></textarea>
+        @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+          <mat-error>
+            @if (configForm.get('config_content')?.errors?.['required']) {
+              Device configuration is required.
+            } @else {
+              Invalid device configuration format.
+            }
+          </mat-error>
+        }
+      </div>
+    }
+    @if ((configForm.value?.config_type === 'text') && configForm.value?.fill_type === 'text') {
+      <div class="form-field-wrapper">
+        <label for="fill_type" class="form-label">Config Content *</label>
+        <textarea matInput formControlName="config_content" cols="40" rows="5"></textarea>
+        @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+          <mat-error>
+            @if (configForm.get('config_content')?.errors?.['required']) {
+              Device configuration is required.
+            } @else {
+              Invalid device configuration format.
+            }
+          </mat-error>
+        }
+      </div>
+    }
+    @if (configForm.value?.fill_type === 'file') {
+      <div class="form-field-wrapper">
+        <label for="fill_type" class="form-label">Upload Config File *</label>
+        <input formControlName="config_file_upload" type="file" (change)="onFileSelected($event)">
+        @if (configForm.get('config_content')?.invalid && configForm.get('config_content')?.touched) {
+          <mat-error>
+            @if (configForm.get('config_content')?.errors?.['required']) {
+              Device configuration is required.
+            } @else {
+              Invalid device configuration format.
+            }
+          </mat-error>
+        }
+      </div>
+    }
+    <div class="form-field-wrapper">
+      <label for="comment" class="form-label">Comment </label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="comment"
+          formControlName="comment"
+          matInput
+          placeholder="Comment"
+          type="text"
+        />
+        @if (configForm.get('comment')?.invalid && configForm.get('comment')?.touched) {
+          <mat-error>
+            @if (configForm.get('comment')?.errors?.['required']) {
+              Comment is required.
+            } @else {
+              Invalid comment format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+  </form>
+</mat-dialog-content>
+<mat-dialog-actions>
+  <button
+    mat-button
+    color="basic"
+    (click)="onCancel()">
+    Cancel
+  </button>
+  <button
+    mat-raised-button
+    color="primary"
+    (click)="onSubmit()">
+    Submit
+  </button>
+</mat-dialog-actions>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.html	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.html	(working copy)
@@ -1 +1,62 @@
-<p>device-custom-config-overview works!</p>
+<div class="button-container">
+  <button mat-raised-button (click)="addCustomConfig()">Add</button>
+</div>
+<div class="table-container">
+  <table mat-table [dataSource]="dataSource" class="mat-elevation-z1">
+    <ng-container matColumnDef="serial">
+      <th mat-header-cell *matHeaderCellDef> No.</th>
+      <td mat-cell *matCellDef="let element; let i = index;"> {{ getGlobalSerial(i) }}</td>
+    </ng-container>
+    <ng-container matColumnDef="deviceName">
+      <th mat-header-cell *matHeaderCellDef>Configuration Filename</th>
+      <td mat-cell *matCellDef="let element">
+        <a class="details-page-link" (click)="editDeviceConfiguration(element)">{{ element.name }}</a>
+      </td>
+    </ng-container>
+    <ng-container matColumnDef="fileType">
+      <th mat-header-cell *matHeaderCellDef>File Type</th>
+      <td mat-cell *matCellDef="let element"> {{ element.file_type }}</td>
+    </ng-container>
+    <ng-container matColumnDef="deviceType">
+      <th mat-header-cell *matHeaderCellDef>Device Type</th>
+      <td mat-cell *matCellDef="let element"> {{ element.device_type }}</td>
+    </ng-container>
+    <ng-container matColumnDef="createTime">
+      <th mat-header-cell *matHeaderCellDef>Create Time</th>
+      <td mat-cell *matCellDef="let element">{{ element.create_time }}</td>
+    </ng-container>
+    <ng-container matColumnDef="cloneSource">
+      <th mat-header-cell *matHeaderCellDef>Modify Time</th>
+      <td mat-cell *matCellDef="let element">{{ element.modify_time }}</td>
+    </ng-container>
+    <ng-container matColumnDef="comment">
+      <th mat-header-cell *matHeaderCellDef>Comment</th>
+      <td mat-cell *matCellDef="let element">{{ element.comment }}</td>
+    </ng-container>
+    <ng-container matColumnDef="action">
+      <th mat-header-cell *matHeaderCellDef class="action-header w-10"> Action</th>
+      <td mat-cell *matCellDef="let element">
+        <div class="row-action a-link">
+          <fa-icon [icon]="['fas', 'code-compare']" size="lg" matTooltip="Compare configuration"
+                   (click)="compareConfiguration(element)"></fa-icon>
+          <fa-icon [icon]="['fas', 'gears']" size="lg" matTooltip="Apply configuration"
+                   (click)="applyConfiguration(element)"></fa-icon>
+          <fa-icon [icon]="['fas', 'list-check']" size="lg" matTooltip="Task for this configuration"
+                   (click)="showTaskConfiguration(element)"></fa-icon>
+          <fa-icon [icon]="['far', 'trash-can']" size="lg" class="delete-icon" matTooltip="Delete configuration"
+                   (click)="deleteConfiguration(element)"></fa-icon>
+        </div>
+      </td>
+    </ng-container>
+    <tr mat-header-row *matHeaderRowDef="deviceColumns"></tr>
+    <tr mat-row *matRowDef="let row; columns: deviceColumns;"></tr>
+    <tr class="mat-row table-no-data" *matNoDataRow>
+      <td class="mat-cell" colspan="11">No results found.</td>
+    </tr>
+  </table>
+  <mat-paginator
+    [pageSizeOptions]="[10, 15, 20, 25]"
+    [length]="totalRecords"
+    showFirstLastButtons
+  ></mat-paginator>
+</div>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-overview/device-custom-config-overview.ts	(working copy)
@@ -1,11 +1,575 @@
-import { Component } from '@angular/core';
+import {AfterViewInit, ChangeDetectorRef, Component, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
+import {SharedModule} from '../../../shared/shared-module';
+import {MatTableDataSource} from '@angular/material/table';
+import {MatPaginator} from '@angular/material/paginator';
+import {MAT_DIALOG_DATA, MatDialog, MatDialogConfig, MatDialogRef} from '@angular/material/dialog';
+import {DeviceService} from '../../../services/device-service';
+import {NotificationService} from '../../../services/notification';
+import {Router} from '@angular/router';
+import {take} from 'rxjs/operators';
+import {
+  ApplyConfigurationDialog,
+  CompareCloneConfigDialog,
+  EditConfigurationDialog
+} from '../device-cloned-files/device-cloned-files';
+import {Confirmation} from '../../../services/confirmation';
+import {ShowTasksByNameDialog} from '../device-backup-files/device-backup-files';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+import {Storage} from '../../../services/storage';
+import {SystemService} from '../../../services/system-service';
 
 @Component({
   selector: 'app-device-custom-config-overview',
-  imports: [],
+  imports: [SharedModule],
   templateUrl: './device-custom-config-overview.html',
   styleUrl: './device-custom-config-overview.scss'
 })
-export class DeviceCustomConfigOverview {
+export class DeviceCustomConfigOverview implements OnInit, AfterViewInit {
+
+  totalRecords: number = 0;
+  dataSource: MatTableDataSource<any> = new MatTableDataSource();
+  deviceColumns: string[] = ['serial', 'deviceName', 'fileType', 'deviceType', 'createTime', 'cloneSource', 'comment', 'action'];
+  @ViewChild(MatPaginator) paginator!: MatPaginator;
+
+  dialog = inject(MatDialog);
+  dialogConfig = new MatDialogConfig();
+  devices = [];
+  groupedDevices: any = {};
+  groups: any = [];
+  deviceGroups: any = [];
+
+  constructor(
+    private _device: DeviceService,
+    private _cdRef: ChangeDetectorRef,
+    private _notification: NotificationService,
+    private _router: Router,
+    private _confirmation: Confirmation,
+  ) {
+  }
+
+  ngOnInit() {
+    setTimeout(() => {
+      this.getDeviceConfigFiles();
+      this.getDeviceGroups();
+      this.getAMPDevicesList();
+    })
+  }
+
+  ngAfterViewInit() {
+    this.dataSource.paginator = this.paginator;
+    this._cdRef.detectChanges();
+  }
+
+  getDeviceConfigFiles() {
+    this.totalRecords = 0;
+    this.dataSource.data = [];
+    this._device.getDeviceConfigFiles().pipe(take(1)).subscribe({
+      next: (result: any) => {
+        result.forEach((entry: any) => {
+          if (entry.file_type === 'customize' || entry.file_type === 'template') {
+            this.dataSource.data.push(entry);
+          }
+        });
+        this.dataSource.paginator = this.paginator;
+        this.totalRecords = this.dataSource.data.length;
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+      }
+    })
+  }
+
+  getAMPDevicesList() {
+    this.devices = [];
+    this._device.getAMPDevicesList().pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.length > 0) {
+          this.devices = result;
+        }
+        this.groupDevicesByType();
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  groupDevicesByType(): void {
+    this.groupedDevices = this.devices.reduce((acc: any, device: any) => {
+      const type = device.type;
+      if (!acc[type]) {
+        acc[type] = [];
+      }
+      acc[type].push(device);
+      return acc;
+    }, {});
+  }
+
+  getDeviceGroups() {
+    this.groups = [];
+    this.deviceGroups = []
+    // ToDo: Update with actual RoleId
+    let roleId = "0"
+    let rawPayload = new FormData();
+    rawPayload.set('action', 'FilterRoleDeviceGroups');
+    rawPayload.set('options', JSON.stringify({"role_id": roleId}));
+    this._device.getDeviceGroups(rawPayload)
+      .pipe(take(1))
+      .subscribe({
+        next: (result: any) => {
+          if (result && result.length > 1) {
+            if (result[1] && 'result' in result[1]) {
+              let groups = result[1].result;
+              groups.forEach((group: any) => {
+                if (group?.device_list.length > 0) {
+                  this.groups.push(group?.group_name);
+                  this.deviceGroups.push(group);
+                }
+              })
+            }
+          }
+        }, error: (error: { message: string; }) => {
+          console.log(error);
+          this._notification.showError(error.message);
+        }
+      })
+  }
+
+  getGlobalSerial(index: number): number {
+    if (this.paginator) {
+      return this.paginator.pageIndex * this.paginator.pageSize + index + 1;
+    }
+    return index + 1;
+  }
+
+  editDeviceConfiguration(_config: any) {
+    this.dialogConfig.data = {
+      config: _config,
+    };
+    const dialogRef = this.dialog.open(EditConfigurationDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe(() => {
+    })
+  }
+
+  compareConfiguration(_config: any) {
+    this.dialogConfig.width = '60%';
+    this.dialogConfig.data = {
+      device: _config,
+      devices: this.groupedDevices[_config?.device_type],
+    }
+    const dialogRef = this.dialog.open(CompareCloneConfigDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+    })
+  }
+
+  applyConfiguration(_config: any) {
+    this.dialogConfig.width = '60%';
+    this.dialogConfig.data = {
+      device: _config,
+      devices: this.groupedDevices[_config?.device_type],
+      groups: this.groups
+    }
+    const dialogRef = this.dialog.open(ApplyConfigurationDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+    })
+  }
+
+  showTaskConfiguration(_config: any) {
+    this.dialogConfig.data = {
+      config: _config,
+    };
+    const dialogRef = this.dialog.open(ShowTasksByNameDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+    })
+  }
+
+  deleteConfiguration(_config: any) {
+    let confirmMsg = `Are you sure you want to delete the configuration file - ${_config?.name}"?`
+    this._confirmation.openConfirmDialog({
+      title: `Delete Configuration File?`,
+      message: confirmMsg,
+      confirmButtonText: 'Yes, Delete It',
+      cancelButtonText: 'No, Keep It',
+      confirmButtonColor: 'warn',
+      cancelButtonColor: 'primary'
+    }).subscribe((result: boolean) => {
+      if (result) {
+        let rawPayload = new FormData();
+        rawPayload.set('pk', JSON.stringify({
+          file_type: _config?.file_type,
+          name: _config?.name
+        }));
+        this._device.deleteDeviceConfigFile(rawPayload)
+          .pipe(take(1))
+          .subscribe({
+            next: (result: any) => {
+              this.getDeviceConfigFiles();
+            },
+            error: (err: any) => {
+              if (err?.status === 200) {
+                // ToDo: Backend fix required.
+                this._notification.showSuccess(`The custom configuration file has been deleted successfully!`);
+                this.getDeviceConfigFiles();
+              } else {
+                this._notification.showError(`Failed to delete the custom configuration file: ${err}.`);
+              }
+            }
+          })
+      }
+    });
+  }
+
+  addCustomConfig() {
+    this.dialogConfig.width = '60%';
+    this.dialogConfig.data = {};
+    this.dialogConfig.disableClose = true;
+    const dialogRef = this.dialog.open(CreateCustomConfigurationDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+      if (result) {
+        this.getDeviceConfigFiles();
+      }
+    })
+  }
+}
+
+@Component({
+  selector: 'create-custom-config',
+  templateUrl: './create-custom-config.html',
+  imports: [SharedModule]
+})
+export class CreateCustomConfigurationDialog implements OnInit, OnDestroy {
+
+  readonly data = inject(MAT_DIALOG_DATA);
+  readonly dialogRef = inject(MatDialogRef<CreateCustomConfigurationDialog>);
+  configForm: FormGroup;
+  deviceTypes: any = [];
+
+  configOption: any = [
+    {value: 'text', label: 'Text Configuration'},
+    {value: 'file', label: 'File Configuration'},
+    {value: 'template', label: 'Template Configuration'},
+  ]
+  selectedFile: File | null = null;
+  fileContent: any = null;
+  upload_path: string = '';
+
+  constructor(
+    private _device: DeviceService,
+    private _notification: NotificationService,
+    private _formBuilder: FormBuilder,
+    private _storage: Storage,
+    private _cdRef: ChangeDetectorRef,
+    private _system: SystemService,
+  ) {
+    this.configForm = this._formBuilder.group({
+      config_type: ['text', [Validators.required]],
+      filename: [''],
+      device_type: ['APV'],
+      fill_type: ['text'],
+      using: ['blank'],
+      config_content: [''],
+      config_file_upload: [''],
+      comment: [''],
+      template_type: ['import'],
+      hostname: [''],
+      cert_index: [''],
+      cert_type: [''],
+      password: [''],
+      servername: [''],
+      template_content: [{value: '', disabled: true}],
+      config_preview: [{value: '', disabled: true}],
+    });
+    this.setupConditionalValidation();
+    this.onTemplateChange();
+  }
+
+  ngOnInit() {
+    let deviceTypes = this._storage.getItem('deviceType')
+    this.deviceTypes = JSON.parse(<string>deviceTypes)['DEVICE_STD_LIST'];
+  }
 
+  setupConditionalValidation(): void {
+    const configTypeControl = this.configForm.get('config_type');
+    const filenameControl = this.configForm.get('filename');
+    const deviceTypeControl = this.configForm.get('device_type');
+    const fillTypeControl = this.configForm.get('fill_type');
+    const configContentControl = this.configForm.get('config_content');
+    const configFileUploadControl = this.configForm.get('config_file_upload');
+    const usingControl = this.configForm.get('using');
+    const templateTypeControl = this.configForm.get('template_type');
+    const hostnameControl = this.configForm.get('hostname');
+    const certIndexControl = this.configForm.get('cert_index');
+    const certTypeControl = this.configForm.get('cert_type');
+    const servernameControl = this.configForm.get('servername');
+    const passwordControl = this.configForm.get('password');
+
+    configTypeControl?.valueChanges.subscribe(configType => {
+      filenameControl?.clearValidators();
+      deviceTypeControl?.clearValidators();
+      fillTypeControl?.clearValidators();
+      configContentControl?.clearValidators();
+      configFileUploadControl?.clearValidators();
+      usingControl?.clearValidators();
+      templateTypeControl?.clearValidators();
+      hostnameControl?.clearValidators();
+      certIndexControl?.clearValidators();
+      certTypeControl?.clearValidators();
+      servernameControl?.clearValidators();
+      passwordControl?.clearValidators();
+
+      if (configType === 'text') {
+        filenameControl?.setValidators(Validators.required);
+        deviceTypeControl?.setValidators(Validators.required);
+        fillTypeControl?.setValidators(Validators.required);
+      } else if (configType === 'template') {
+        filenameControl?.setValidators(Validators.required);
+        deviceTypeControl?.setValidators(Validators.required);
+        configContentControl?.setValidators(Validators.required);
+      } else if (configType === 'file') {
+        filenameControl?.setValidators(Validators.required);
+        deviceTypeControl?.setValidators(Validators.required);
+        configContentControl?.setValidators(Validators.required);
+        configFileUploadControl?.setValidators(Validators.required);
+      }
+
+      filenameControl?.updateValueAndValidity();
+      deviceTypeControl?.updateValueAndValidity();
+      fillTypeControl?.updateValueAndValidity();
+      configContentControl?.updateValueAndValidity();
+      configFileUploadControl?.updateValueAndValidity();
+      usingControl?.updateValueAndValidity();
+      templateTypeControl?.updateValueAndValidity();
+      hostnameControl?.updateValueAndValidity();
+      certIndexControl?.updateValueAndValidity();
+      certTypeControl?.updateValueAndValidity();
+      servernameControl?.updateValueAndValidity();
+      passwordControl?.updateValueAndValidity();
+    });
+
+    fillTypeControl?.valueChanges.subscribe(fillType => {
+      if (configTypeControl?.value === 'text') {
+        if (fillType === 'text') {
+          configContentControl?.setValidators(Validators.required);
+          configFileUploadControl?.clearValidators();
+        } else if (fillType === 'file') {
+          configFileUploadControl?.setValidators(Validators.required);
+          configContentControl?.clearValidators();
+        }
+        configContentControl?.updateValueAndValidity();
+        configFileUploadControl?.updateValueAndValidity();
+      }
+    });
+
+    deviceTypeControl?.valueChanges.subscribe(deviceType => {
+      if (configTypeControl?.value === 'file') {
+        if (deviceType === 'AG' || deviceType === 'vxAG') {
+          configContentControl?.setValidators(Validators.required);
+        } else {
+          configContentControl?.clearValidators();
+        }
+        configContentControl?.updateValueAndValidity();
+
+        if (['APV', 'vAPV', 'ASF', 'vASF'].includes(deviceType)) {
+          usingControl?.setValidators(Validators.required);
+        } else {
+          usingControl?.clearValidators();
+        }
+        usingControl?.updateValueAndValidity();
+      }
+    });
+
+    usingControl?.valueChanges.subscribe(usingValue => {
+      if (['APV', 'vAPV', 'ASF', 'vASF'].includes(deviceTypeControl?.value)) {
+        if (usingValue === 'blank') {
+          configContentControl?.setValidators(Validators.required);
+          templateTypeControl?.clearValidators();
+          hostnameControl?.clearValidators();
+          certIndexControl?.clearValidators();
+          certTypeControl?.clearValidators();
+          servernameControl?.clearValidators();
+          passwordControl?.clearValidators();
+        } else if (usingValue === 'template') {
+          configContentControl?.clearValidators();
+          templateTypeControl?.setValidators(Validators.required);
+          hostnameControl?.setValidators(Validators.required);
+          certIndexControl?.setValidators(Validators.required);
+          certTypeControl?.setValidators(Validators.required);
+          servernameControl?.setValidators(Validators.required);
+          passwordControl?.setValidators(Validators.required);
+        } else {
+          configContentControl?.clearValidators();
+          templateTypeControl?.clearValidators();
+          hostnameControl?.clearValidators();
+          certIndexControl?.clearValidators();
+          certTypeControl?.clearValidators();
+          servernameControl?.clearValidators();
+          passwordControl?.clearValidators();
+        }
+        configContentControl?.updateValueAndValidity();
+        templateTypeControl?.updateValueAndValidity();
+        hostnameControl?.updateValueAndValidity();
+        certIndexControl?.updateValueAndValidity();
+        certTypeControl?.updateValueAndValidity();
+        servernameControl?.updateValueAndValidity();
+        passwordControl?.updateValueAndValidity();
+      }
+    });
+    configTypeControl?.updateValueAndValidity();
+    fillTypeControl?.updateValueAndValidity();
+    deviceTypeControl?.updateValueAndValidity();
+    usingControl?.updateValueAndValidity();
+  }
+
+  onFillTypeChange() {
+  }
+
+  onFileSelected(event: Event): void {
+    const input = event.target as HTMLInputElement;
+    const file = input.files?.[0];
+
+    if (!file) return;
+
+    this.selectedFile = file;
+    this.fileContent = null;
+
+    console.log('Selected file:', {
+      name: file.name,
+      type: file.type,
+      size: file.size
+    });
+
+    const reader = new FileReader();
+
+    reader.onload = () => {
+      const result = reader.result;
+      if (!result) return;
+
+      if (file.type === 'application/json') {
+        try {
+          this.fileContent = JSON.parse(result as string);
+        } catch {
+          this._notification.showError('Invalid JSON file.');
+        }
+      } else {
+        this.fileContent = result;
+      }
+      this.configForm.patchValue({
+        config_content: this.fileContent,
+      })
+    };
+
+    reader.onerror = () => {
+      console.error('File reading error:', reader.error);
+      this._notification.showError('Error reading file.');
+    };
+  }
+
+  onFileUploadToServer(event: Event): void {
+    const input = event.target as HTMLInputElement;
+    const file = input.files?.[0];
+
+    if (!file) return;
+
+    this.selectedFile = file;
+    this.fileContent = null;
+
+    let payload = new FormData();
+    payload.append('file', file, file.name);
+    this._system.upload(null, payload)
+      .pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result.files) {
+          if (result.files.length > 0) {
+            this.upload_path = result.files[0].url;
+          }
+        }
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+      }
+    })
+  }
+
+  onTemplateChange(): void {
+    if (this.configForm.value.template_type === 'import') {
+      this.configForm.patchValue({
+        template_content: ''
+          + '# The CLI for importing certificate\n'
+          + 'ssl import pem "{<HostName>}" {<CertIndex>} "{[ServerNameIndification]}" ##URL## "{[Pssword]}"\n'
+          + '\n'
+          + '# Check current certificate \n'
+          + 'show ssl import pem match "{<HostName>}" {<CertIndex>} "{[ServerNameIndification]}" "{<CertType>}" ##URL## "{[Pssword]}"\n',
+        config_preview: ''
+          + '# The CLI for importing certificate\n'
+          + 'ssl import pem "{<HostName>}" {<CertIndex>} "" ##URL## ""\n'
+          + '\n'
+          + '# Check current certificate\n'
+          + 'show ssl import pem match "{<HostName>}" {<CertIndex>} "" "{<CertType>}" ##URL## ""',
+      })
+    } else if (this.configForm.value.template_type === 'vhost') {
+      this.configForm.patchValue({
+        template_content: ''
+          + 'show ssl import pem match "{<HostName>}" {<CertIndex>} "{[ServerNameIndification]}" "{<CertType>}" ##URL## "{[Pssword]}"\n',
+        config_preview: ''
+          + 'show ssl import pem match "{<HostName>}" {<CertIndex>} "" "{<CertType>}" ##URL## ""',
+      })
+    }
+  }
+
+
+  onSubmit(): void {
+    if (this.configForm.invalid) {
+      console.log(this.configForm.value);
+      this.configForm.markAllAsTouched();
+      return;
+    }
+    let payload = new FormData();
+    let rawPayload: any = {};
+    rawPayload = {
+      name: this.configForm.value.filename,
+      file_type: this.configForm.value.config_type === 'template' ? 'template' : 'customize',
+      device_type: this.configForm.value.device_type,
+      comment: this.configForm.value.comment,
+      cli_content: this.configForm.value.config_content,
+    }
+    if (this.configForm.value.config_type === 'file') {
+      rawPayload.upload_path = this.upload_path;
+      rawPayload.upload_type = true;
+      if (this.configForm.value.using === 'template') {
+        let _payload = `# The CLI for importing certificate \nssl import pem "${this.configForm.value.hostname}" ${this.configForm.value.cert_index} "${this.configForm.value.servername}" ##URL## "${this.configForm.value.password}"\n\n# Check current certificate \nshow ssl import pem match "${this.configForm.value.hostname}" ${this.configForm.value.cert_index} "${this.configForm.value.servername}" "${this.configForm.value.cert_type}" ##URL## "${this.configForm.value.password}"\n`;
+        if (this.configForm.value.template_type === 'vhost') {
+          _payload = `show ssl import pem match "${this.configForm.value.hostname}" ${this.configForm.value.cert_index} "${this.configForm.value.servername}" "${this.configForm.value.cert_type}" ##URL## "${this.configForm.value.password}"\n`
+        }
+        rawPayload.cli_content = _payload;
+      }
+    }
+
+    payload.set('post_data', JSON.stringify(rawPayload));
+    this._device.addCustomConfigFile(payload)
+      .pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result[0]) {
+          this._notification.showSuccess(`The custom configuration has been updated successfully.`);
+          this.dialogRef.close(true);
+        } else {
+          this._notification.showError(`Failed to add the custom configuration: ${result[1]}`);
+        }
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  ngOnDestroy() {
+  }
+
+  onCancel(): void {
+    this.dialogRef.close(false);
+  }
 }
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/create-template-keys.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/create-template-keys.html	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/create-template-keys.html	(working copy)
@@ -0,0 +1,85 @@
+<h2 mat-dialog-title>Create Template Keys</h2>
+<mat-dialog-content>
+  <form
+    (ngSubmit)="onSubmit()"
+    [formGroup]="configForm"
+  >
+    <div class="form-field-wrapper">
+      <label for="keyword" class="form-label">Template keyword *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="keyword"
+          formControlName="keyword"
+          matInput
+          placeholder="Keyword"
+          type="text"
+        />
+        @if (configForm.get('keyword')?.invalid && configForm.get('keyword')?.touched) {
+          <mat-error>
+            @if (configForm.get('keyword')?.errors?.['required']) {
+              Keyword is required.
+            } @else {
+              Invalid keyword format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="default_value" class="form-label">Default value *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="default_value"
+          formControlName="default_value"
+          matInput
+          placeholder="Default Value"
+          type="text"
+        />
+        @if (configForm.get('default_value')?.invalid && configForm.get('default_value')?.touched) {
+          <mat-error>
+            @if (configForm.get('default_value')?.errors?.['required']) {
+              Default value is required.
+            } @else {
+              Invalid default value format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="description" class="form-label">Description</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="description"
+          formControlName="description"
+          matInput
+          placeholder="Description"
+          type="text"
+        />
+        @if (configForm.get('description')?.invalid && configForm.get('description')?.touched) {
+          <mat-error>
+            @if (configForm.get('description')?.errors?.['required']) {
+              Description is required.
+            } @else {
+              Invalid description format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+  </form>
+</mat-dialog-content>
+<mat-dialog-actions>
+  <button
+    mat-button
+    color="basic"
+    (click)="onCancel()">
+    Cancel
+  </button>
+  <button
+    mat-raised-button
+    color="primary"
+    (click)="onSubmit()">
+    Submit
+  </button>
+</mat-dialog-actions>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.html	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.html	(working copy)
@@ -0,0 +1,96 @@
+<mat-card class="page-card-1" appearance="filled">
+  <mat-card-header>
+    <mat-card-title>Templates</mat-card-title>
+  </mat-card-header>
+  <div class="button-container">
+    <button mat-raised-button (click)="addCustomTemplate()">Add</button>
+  </div>
+  <div class="table-container">
+    <table mat-table [dataSource]="dataSource" class="mat-elevation-z1">
+      <ng-container matColumnDef="serial">
+        <th mat-header-cell *matHeaderCellDef> No.</th>
+        <td mat-cell *matCellDef="let element; let i = index;"> {{ getGlobalSerial(i) }}</td>
+      </ng-container>
+      <ng-container matColumnDef="tKey">
+        <th mat-header-cell *matHeaderCellDef>Template Keyword</th>
+        <td mat-cell *matCellDef="let element"> {{ element.key }}</td>
+      </ng-container>
+      <ng-container matColumnDef="tDescription">
+        <th mat-header-cell *matHeaderCellDef>Description</th>
+        <td mat-cell *matCellDef="let element"> {{ element.description }}</td>
+      </ng-container>
+      <ng-container matColumnDef="tDefault">
+        <th mat-header-cell *matHeaderCellDef>Default Value</th>
+        <td mat-cell *matCellDef="let element">{{ element.default_value }}</td>
+      </ng-container>
+      <ng-container matColumnDef="action">
+        <th mat-header-cell *matHeaderCellDef class="action-header w-10"> Action</th>
+        <td mat-cell *matCellDef="let element">
+          <div class="row-action a-link">
+            <fa-icon [icon]="['far', 'edit']" size="lg" matTooltip="Edit"
+                     (click)="editTemplateKey(element)"></fa-icon>
+            <fa-icon [icon]="['far', 'trash-can']" size="lg" class="delete-icon" matTooltip="Delete"
+                     (click)="deleteTemplateKey(element)"></fa-icon>
+          </div>
+        </td>
+      </ng-container>
+      <tr mat-header-row *matHeaderRowDef="deviceColumns"></tr>
+      <tr mat-row *matRowDef="let row; columns: deviceColumns;"></tr>
+      <tr class="mat-row table-no-data" *matNoDataRow>
+        <td class="mat-cell" colspan="11">No results found.</td>
+      </tr>
+    </table>
+    <mat-paginator
+      [pageSizeOptions]="[10, 15, 20, 25]"
+      [length]="totalRecords"
+      showFirstLastButtons
+    ></mat-paginator>
+  </div>
+</mat-card>
+
+<mat-card class="page-card-1" appearance="filled">
+  <mat-card-header>
+    <mat-card-title>Device Template Parameters</mat-card-title>
+  </mat-card-header>
+  <div class="table-container">
+    <table mat-table [dataSource]="dataSource1" class="mat-elevation-z1">
+      <ng-container matColumnDef="serial">
+        <th mat-header-cell *matHeaderCellDef> No.</th>
+        <td mat-cell *matCellDef="let element; let i = index;"> {{ getGlobalSerial(i) }}</td>
+      </ng-container>
+      <ng-container matColumnDef="device">
+        <th mat-header-cell *matHeaderCellDef>Device IP</th>
+        <td mat-cell *matCellDef="let element"> {{ element?.device_ip }}</td>
+      </ng-container>
+      <ng-container matColumnDef="template">
+        <th mat-header-cell *matHeaderCellDef>Template</th>
+        <td mat-cell *matCellDef="let element">
+          @for (key of Object.keys(element); track key) {
+            @if (key !== 'device_ip') {
+              <span> {{ key }} : {{ element[key] }}</span><br>
+            }
+          }
+        </td>
+      </ng-container>
+      <ng-container matColumnDef="action">
+        <th mat-header-cell *matHeaderCellDef class="action-header w-10"> Action</th>
+        <td mat-cell *matCellDef="let element">
+          <div class="row-action a-link">
+            <fa-icon [icon]="['far', 'edit']" size="lg" matTooltip="Edit"
+                     (click)="editDeviceTemplateConfig(element)"></fa-icon>
+          </div>
+        </td>
+      </ng-container>
+      <tr mat-header-row *matHeaderRowDef="deviceColumns1"></tr>
+      <tr mat-row *matRowDef="let row; columns: deviceColumns1;"></tr>
+      <tr class="mat-row table-no-data" *matNoDataRow>
+        <td class="mat-cell" colspan="11">No results found.</td>
+      </tr>
+    </table>
+    <mat-paginator
+      [pageSizeOptions]="[10, 15, 20, 25]"
+      [length]="totalRecords1"
+      showFirstLastButtons
+    ></mat-paginator>
+  </div>
+</mat-card>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.scss
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.scss	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.scss	(working copy)
@@ -0,0 +1,20 @@
+.page-card-1 {
+  width: 100%;
+  border-radius: 0;
+  background-color: inherit;
+  font-size: 14px !important;
+  mat-card-header {
+    color: #1170cf;
+  }
+}
+
+mat-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 10px;
+}
+
+mat-card-title {
+  font-size: medium;
+}
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.spec.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.spec.ts	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.spec.ts	(working copy)
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeviceCustomConfigTemplates } from './device-custom-config-templates';
+
+describe('DeviceCustomConfigTemplates', () => {
+  let component: DeviceCustomConfigTemplates;
+  let fixture: ComponentFixture<DeviceCustomConfigTemplates>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [DeviceCustomConfigTemplates]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(DeviceCustomConfigTemplates);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.ts	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/device-custom-config-templates.ts	(working copy)
@@ -0,0 +1,376 @@
+import {AfterViewInit, ChangeDetectorRef, Component, inject, OnInit, ViewChild} from '@angular/core';
+import {MatTableDataSource} from '@angular/material/table';
+import {MatPaginator} from '@angular/material/paginator';
+import {MAT_DIALOG_DATA, MatDialog, MatDialogConfig, MatDialogRef} from '@angular/material/dialog';
+import {DeviceService} from '../../../services/device-service';
+import {NotificationService} from '../../../services/notification';
+import {Router} from '@angular/router';
+import {Confirmation} from '../../../services/confirmation';
+import {take} from 'rxjs/operators';
+import {SharedModule} from '../../../shared/shared-module';
+import {FormBuilder, FormGroup, Validators} from '@angular/forms';
+
+@Component({
+  selector: 'app-device-custom-config-templates',
+  imports: [SharedModule],
+  templateUrl: './device-custom-config-templates.html',
+  styleUrl: './device-custom-config-templates.scss'
+})
+export class DeviceCustomConfigTemplates implements OnInit, AfterViewInit {
+
+  totalRecords: number = 0;
+  dataSource: MatTableDataSource<any> = new MatTableDataSource();
+  deviceColumns: string[] = ['serial', 'tKey', 'tDescription', 'tDefault', 'action'];
+  @ViewChild(MatPaginator) paginator!: MatPaginator;
+
+  totalRecords1: number = 0;
+  dataSource1: MatTableDataSource<any> = new MatTableDataSource();
+  deviceColumns1: string[] = ['serial', 'device', 'template', 'action'];
+  @ViewChild(MatPaginator) paginator1!: MatPaginator;
+
+  dialog = inject(MatDialog);
+  dialogConfig = new MatDialogConfig();
+  devices = [];
+  groupedDevices: any = {};
+  groups: any = [];
+  deviceGroups: any = [];
+  protected readonly Object = Object;
+
+  constructor(
+    private _device: DeviceService,
+    private _cdRef: ChangeDetectorRef,
+    private _notification: NotificationService,
+    private _router: Router,
+    private _confirmation: Confirmation,
+  ) {
+  }
+
+  ngOnInit() {
+    setTimeout(() => {
+      this.getConfigTemplateKey();
+      this.getTemplateKeyPairs();
+    })
+  }
+
+  ngAfterViewInit() {
+    this.dataSource.paginator = this.paginator;
+    this._cdRef.detectChanges();
+  }
+
+  getConfigTemplateKey() {
+    this.dataSource.data = [];
+    this.totalRecords = 0;
+    this._device.getConfigTemplateKey().pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.length > 0) {
+          this.dataSource.data = result;
+          this.dataSource.paginator = this.paginator;
+          this.totalRecords = this.dataSource.data.length;
+        }
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  getTemplateKeyPairs() {
+    this.totalRecords1 = 0;
+    this.dataSource1.data = [];
+    this._device.getTemplateKeyPairs().pipe(take(1)).subscribe({
+      next: (result: any) => {
+        const keys = result.key_list;
+        const values = result.value_list;
+        const resultArray = [];
+        for (const row of values) {
+          const rowObject: any = {};
+          for (let i = 0; i < keys.length; i++) {
+            rowObject[keys[i]] = row[i];
+          }
+          resultArray.push(rowObject);
+        }
+        this.dataSource1.data = resultArray;
+        this.dataSource1.paginator = this.paginator1;
+        this.totalRecords1 = this.dataSource1.data.length;
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  getGlobalSerial(index: number): number {
+    if (this.paginator) {
+      return this.paginator.pageIndex * this.paginator.pageSize + index + 1;
+    }
+    return index + 1;
+  }
+
+  addCustomTemplate(): void {
+    this.dialogConfig.data = {};
+    const dialogRef = this.dialog.open(CreateTemplateKeysDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+      if (result) {
+        this.getConfigTemplateKey();
+        this.getTemplateKeyPairs();
+      }
+    })
+  }
+
+  editTemplateKey(_template: any) {
+    this.dialogConfig.data = _template;
+    const dialogRef = this.dialog.open(EditTemplateKeysDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+      if (result) {
+        this.getConfigTemplateKey();
+        this.getTemplateKeyPairs();
+      }
+    })
+  }
+
+  editDeviceTemplateConfig(_config: any) {
+    this.dialogConfig.data = _config;
+    const dialogRef = this.dialog.open(EditDeviceTemplateKeyDefaultValueDialog, this.dialogConfig);
+    dialogRef.afterClosed().subscribe((result: boolean) => {
+      if (result) {
+        this.getTemplateKeyPairs();
+      }
+    })
+  }
+
+  deleteTemplateKey(_template: any) {
+    let confirmMsg = `Are you sure you want to delete the template keyword - ${_template?.key}"?`
+    this._confirmation.openConfirmDialog({
+      title: `Delete Configuration File?`,
+      message: confirmMsg,
+      confirmButtonText: 'Yes, Delete It',
+      cancelButtonText: 'No, Keep It',
+      confirmButtonColor: 'warn',
+      cancelButtonColor: 'primary'
+    }).subscribe((result: boolean) => {
+      if (result) {
+        let rawPayload = new FormData();
+        rawPayload.set('post_data', JSON.stringify({
+          key: _template?.key
+        }));
+        this._device.deleteConfigTemplateKey(rawPayload)
+          .pipe(take(1))
+          .subscribe({
+            next: (result: any) => {
+              this.getConfigTemplateKey();
+              this.getTemplateKeyPairs();
+            },
+            error: (err: any) => {
+              if (err?.status === 200) {
+                // ToDo: Backend fix required.
+                this._notification.showSuccess(`The template key has been deleted successfully!`);
+                this.getConfigTemplateKey();
+                this.getTemplateKeyPairs();
+              } else {
+                this._notification.showError(`Failed to delete the template key: ${err}.`);
+              }
+            }
+          })
+      }
+    });
+  }
+}
+
+@Component({
+  selector: 'create-template-keys',
+  templateUrl: './create-template-keys.html',
+  imports: [SharedModule]
+})
+export class CreateTemplateKeysDialog {
+
+  readonly data = inject(MAT_DIALOG_DATA);
+  readonly dialogRef = inject(MatDialogRef<CreateTemplateKeysDialog>);
+  configForm: FormGroup;
+
+  constructor(
+    private _device: DeviceService,
+    private _notification: NotificationService,
+    private _formBuilder: FormBuilder,
+    private _cdRef: ChangeDetectorRef,
+  ) {
+    this.configForm = this._formBuilder.group({
+      keyword: ['', [Validators.required]],
+      default_value: ['', [Validators.required]],
+      description: ['']
+    })
+  }
+
+  onSubmit() {
+    if (this.configForm.invalid) {
+      console.log(this.configForm.value);
+      this.configForm.markAllAsTouched();
+      return;
+    }
+    let payload = new FormData();
+    payload.set('post_data', JSON.stringify({
+      key: this.configForm.value.keyword,
+      default_value: this.configForm.value.default_value,
+      description: this.configForm.value.description,
+    }));
+    this._device.addConfigTemplateKey(payload)
+      .pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.state) {
+          this._notification.showSuccess(`Template key has been added successfully.`);
+          this.dialogRef.close(true);
+        }
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  onCancel() {
+    this.dialogRef.close(false);
+  }
+}
+
+@Component({
+  selector: 'edit-template-keys',
+  templateUrl: './edit-template-keys.html',
+  imports: [SharedModule]
+})
+export class EditTemplateKeysDialog {
+
+  readonly data = inject(MAT_DIALOG_DATA);
+  readonly dialogRef = inject(MatDialogRef<EditTemplateKeysDialog>);
+  configForm: FormGroup;
+
+  constructor(
+    private _device: DeviceService,
+    private _notification: NotificationService,
+    private _formBuilder: FormBuilder,
+    private _cdRef: ChangeDetectorRef,
+  ) {
+    this.configForm = this._formBuilder.group({
+      keyword: ['', [Validators.required]],
+      default_value: ['', [Validators.required]],
+      replace_device_default_value: ["no", [Validators.required]],
+      description: [''],
+    })
+    this.configForm.patchValue({
+      keyword: this.data?.key,
+      default_value: this.data?.default_value,
+      description: this.data?.description,
+    })
+  }
+
+  onSubmit() {
+    if (this.configForm.invalid) {
+      console.log(this.configForm.value);
+      this.configForm.markAllAsTouched();
+      return;
+    }
+    let payload = new FormData();
+    payload.set('post_data', JSON.stringify({
+      key: this.configForm.value.keyword,
+      default_value: this.configForm.value.default_value,
+      description: this.configForm.value.description,
+      replace: this.configForm.value.replace_device_default_value === "yes",
+    }));
+    this._device.updateConfigTemplateKey(payload)
+      .pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.state) {
+          this._notification.showSuccess(`Template key has been updated successfully.`);
+          this.dialogRef.close(true);
+        }
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  onCancel() {
+    this.dialogRef.close(false);
+  }
+}
+
+@Component({
+  selector: 'edit-device-template-key-default-value',
+  templateUrl: './edit-device-template-key-default-value.html',
+  imports: [SharedModule]
+})
+export class EditDeviceTemplateKeyDefaultValueDialog implements OnInit {
+
+  readonly data = inject(MAT_DIALOG_DATA);
+  readonly dialogRef = inject(MatDialogRef<EditDeviceTemplateKeyDefaultValueDialog>);
+  configForm!: FormGroup;
+  formKeys!: string[];
+
+  constructor(
+    private _device: DeviceService,
+    private _notification: NotificationService,
+    private _formBuilder: FormBuilder,
+    private _cdRef: ChangeDetectorRef,
+  ) {
+  }
+
+  ngOnInit(): void {
+    this.createDynamicForm();
+  }
+
+  createDynamicForm(): void {
+    const formGroupConfig: { [key: string]: any } = {};
+    this.formKeys = Object.keys(this.data);
+
+    for (const key of this.formKeys) {
+      const value = (this.data as any)[key];
+      if (key === 'device_ip') {
+        formGroupConfig[key] = [{ value: value, disabled: true }, Validators.required];
+      } else {
+        formGroupConfig[key] = [value, [Validators.required]];
+      }
+    }
+
+    this.configForm = this._formBuilder.group(formGroupConfig);
+  }
+
+  onSubmit() {
+    if (this.configForm.invalid) {
+      console.log(this.configForm.value);
+      this.configForm.markAllAsTouched();
+      return;
+    }
+    let payload = new FormData();
+    let rawPayload: any = [];
+    let _obj = this.configForm.getRawValue();
+    Object.keys(_obj).forEach((key: any) => {
+      rawPayload.push({key: key, value: _obj[key]});
+    })
+    payload.set('post_data', JSON.stringify(rawPayload));
+    this._device.updateDeviceConfigTemplateDefaultValue(payload)
+      .pipe(take(1)).subscribe({
+      next: (result: any) => {
+        if (result && result.state) {
+          this._notification.showSuccess(`The device template key - default value has been updated successfully.`);
+          this.dialogRef.close(true);
+        }
+        this._cdRef.detectChanges();
+      },
+      error: error => {
+        this._notification.showError(`Error: ${error?.message}`);
+        this._cdRef.detectChanges();
+      }
+    })
+  }
+
+  onCancel() {
+    this.dialogRef.close(false);
+  }
+}
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-device-template-key-default-value.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-device-template-key-default-value.html	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-device-template-key-default-value.html	(working copy)
@@ -0,0 +1,36 @@
+<h2 mat-dialog-title>Edit Device Template Key Default Value</h2>
+<mat-dialog-content>
+  <form
+    (ngSubmit)="onSubmit()"
+    [formGroup]="configForm"
+  >
+    @for (formElement of formKeys; track formElement) {
+      <div class="form-field-wrapper">
+        <label for="formElement" class="form-label">{{ formElement }}</label>
+        <mat-form-field appearance="outline" subscriptSizing="dynamic">
+          <mat-label>{{ formElement | titlecase }}</mat-label>
+          <input [id]="formElement" matInput [formControlName]="formElement">
+          <mat-error>
+            @if (configForm.get(formElement)?.errors?.['required'] && configForm.get(formElement)?.touched) {
+              {{ formElement | titlecase }} is required.
+            }
+          </mat-error>
+        </mat-form-field>
+      </div>
+    }
+  </form>
+</mat-dialog-content>
+<mat-dialog-actions>
+  <button
+    mat-button
+    color="basic"
+    (click)="onCancel()">
+    Cancel
+  </button>
+  <button
+    mat-raised-button
+    color="primary"
+    (click)="onSubmit()">
+    Submit
+  </button>
+</mat-dialog-actions>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-template-keys.html
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-template-keys.html	(nonexistent)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/components/sub-components/device-custom-config-templates/edit-template-keys.html	(working copy)
@@ -0,0 +1,92 @@
+<h2 mat-dialog-title>Edit Template Keys - {{data?.key}}</h2>
+<mat-dialog-content>
+  <form
+    (ngSubmit)="onSubmit()"
+    [formGroup]="configForm"
+  >
+    <div class="form-field-wrapper">
+      <label for="keyword" class="form-label">Template keyword *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="keyword"
+          formControlName="keyword"
+          matInput
+          placeholder="Keyword"
+          type="text"
+        />
+        @if (configForm.get('keyword')?.invalid && configForm.get('keyword')?.touched) {
+          <mat-error>
+            @if (configForm.get('keyword')?.errors?.['required']) {
+              Keyword is required.
+            } @else {
+              Invalid keyword format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="default_value" class="form-label">Default value *</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="default_value"
+          formControlName="default_value"
+          matInput
+          placeholder="Default Value"
+          type="text"
+        />
+        @if (configForm.get('default_value')?.invalid && configForm.get('default_value')?.touched) {
+          <mat-error>
+            @if (configForm.get('default_value')?.errors?.['required']) {
+              Default value is required.
+            } @else {
+              Invalid default value format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="description" class="form-label">Description</label>
+      <mat-form-field appearance="outline" subscriptSizing="dynamic">
+        <input
+          id="description"
+          formControlName="description"
+          matInput
+          placeholder="Description"
+          type="text"
+        />
+        @if (configForm.get('description')?.invalid && configForm.get('description')?.touched) {
+          <mat-error>
+            @if (configForm.get('description')?.errors?.['required']) {
+              Description is required.
+            } @else {
+              Invalid description format.
+            }
+          </mat-error>
+        }
+      </mat-form-field>
+    </div>
+    <div class="form-field-wrapper">
+      <label for="replace_device_default_value" class="form-label">Replace Device Default Value *</label>
+      <mat-radio-group formControlName="replace_device_default_value">
+        <mat-radio-button value="yes">Yes</mat-radio-button>
+        <mat-radio-button value="no">No</mat-radio-button>
+      </mat-radio-group>
+    </div>
+  </form>
+</mat-dialog-content>
+<mat-dialog-actions>
+  <button
+    mat-button
+    color="basic"
+    (click)="onCancel()">
+    Cancel
+  </button>
+  <button
+    mat-raised-button
+    color="primary"
+    (click)="onSubmit()">
+    Submit
+  </button>
+</mat-dialog-actions>
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/constants/api_urls.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/constants/api_urls.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/constants/api_urls.ts	(working copy)
@@ -111,9 +111,17 @@
   GET_APV_VIRTUAL_SERVICES_URL: `${PREFIX}/proxy_req_dev/rest/loadbalancing/slb/vs/VirtualService`,
   GET_SCHEDULE_DEVICE_BACKUP_URL: `${PREFIX}/cm/configuration/device/get_schedule_backup_all`,
   CLEAR_SCHEDULE_DEVICE_BACKUP_URL: `${PREFIX}/cm/configuration/device/clear_schedule_backup_all`,
-  GET_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL: `${PREFIX}/cm/configuration/configuration_file/get_config/system`,
-  UPDATE_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL: `${PREFIX}cm/configuration/configuration_file/save_config/system`,
+  GET_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL: `${PREFIX}/cm/configuration/configuration_file/get_config`,
+  UPDATE_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL: `${PREFIX}cm/configuration/configuration_file/save_config`,
   GET_AG_CONFIG_ASSOCIATED_VSITE_URL: `${PREFIX}/api/cm/configuration/config_file/TarFile/_get_asso_list_data`,
   GET_AG_VSITE_CA_CONFIG_URL: `${PREFIX}/cm/configuration/configuration_file/get_config/system`,
   UPDATE_AG_VSITE_CA_CONFIG_URL: `${PREFIX}/cm/configuration/configuration_file/save_config/system`,
+  ADD_CUSTOM_CONFIG_FILE_URL: `${PREFIX}/api/cm/configuration/config_file/ConfigFile/_add`,
+  COMMON_UPLOAD_URL: `${PREFIX}/upload`,
+  GET_CONFIG_TEMPLATE_KEY_URL: `${PREFIX}/cm/get_config_template_key`,
+  GET_CONFIG_TEMPLATE_KEY_PAIRS_URL: `${PREFIX}/cm/query_all_tmpkeyval`,
+  DELETE_CONFIG_TEMPLATE_KEY_URL: `${PREFIX}/cm/delete_config_template_key`,
+  ADD_CONFIG_TEMPLATE_KEY_URL: `${PREFIX}/cm/add_config_template_key`,
+  UPDATE_CONFIG_TEMPLATE_KEY_URL: `${PREFIX}/cm/edit_config_template_key`,
+  UPDATE_DEVICE_CONFIG_TEMPLATE_DEFAULT_VALUE_URL: `${PREFIX}/cm/edit_config_template_default_value`,
 } as const; // Makes properties readonly
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/device-service.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/device-service.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/device-service.ts	(working copy)
@@ -135,8 +135,10 @@
     return this.http.get(URLS.GET_AMP_DEVICES_LIST_URL);
   }
 
-  getDeviceConfigByConfigFileName(filename: any) {
-    return this.http.get(`${URLS.GET_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL}/${filename}`);
+  getDeviceConfigByConfigFileName(filename: any, filetype: string = 'system') {
+    let baseURL: string =`${URLS.GET_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL}`;
+    filetype = filetype === 'device' ? 'system' : filetype;
+    return this.http.get(`${baseURL}/${filetype}/${filename}`);
   }
 
   getAGConfigAssociatedVsites(rawPayload: any) {
@@ -159,8 +161,10 @@
     });
   }
 
-  updateDeviceConfigByConfigFileName(filename: string, rawPayload: any) {
-    return this.http.post(`${URLS.UPDATE_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL}/${filename}`, rawPayload, {
+  updateDeviceConfigByConfigFileName(filename: string, filetype: string, rawPayload: any) {
+    let baseURL: string =`${URLS.UPDATE_DEVICE_CONFIG_BY_CONFIG_FILE_NAME_URL}`;
+    filetype = filetype === 'device' ? 'system' : filetype;
+    return this.http.post(`${baseURL}/${filetype}/${filename}`, rawPayload, {
       csrf: true,
       isFormData: true,
       csrfInFormData: true
@@ -179,4 +183,51 @@
     });
   }
 
+  addCustomConfigFile(rawPayload: any) {
+    return this.http.post(URLS.ADD_CUSTOM_CONFIG_FILE_URL, rawPayload, {
+      csrf: true,
+      isFormData: true,
+      csrfInFormData: true
+    });
+  }
+
+  addConfigTemplateKey(rawPayload: any) {
+    return this.http.post(URLS.ADD_CONFIG_TEMPLATE_KEY_URL, rawPayload, {
+      csrf: true,
+      isFormData: true,
+      csrfInFormData: true
+    });
+  }
+
+  updateConfigTemplateKey(rawPayload: any) {
+    return this.http.post(URLS.UPDATE_CONFIG_TEMPLATE_KEY_URL, rawPayload, {
+      csrf: true,
+      isFormData: true,
+      csrfInFormData: true
+    });
+  }
+
+  deleteConfigTemplateKey(rawPayload: any) {
+    return this.http.post(URLS.DELETE_CONFIG_TEMPLATE_KEY_URL, rawPayload, {
+      csrf: true,
+      isFormData: true,
+      csrfInFormData: true
+    });
+  }
+
+  updateDeviceConfigTemplateDefaultValue(rawPayload: any) {
+    return this.http.post(URLS.UPDATE_DEVICE_CONFIG_TEMPLATE_DEFAULT_VALUE_URL, rawPayload, {
+      csrf: true,
+      isFormData: true,
+      csrfInFormData: true
+    });
+  }
+
+  getConfigTemplateKey() {
+    return this.http.get(URLS.GET_CONFIG_TEMPLATE_KEY_URL);
+  }
+
+  getTemplateKeyPairs() {
+    return this.http.get(URLS.GET_CONFIG_TEMPLATE_KEY_PAIRS_URL);
+  }
 }
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/http.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/http.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/http.ts	(working copy)
@@ -6,13 +6,13 @@
 } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Observable } from 'rxjs';
-import {ADD_CSRF_TO_FORMDATA, ADD_CSRF_TO_PAYLOAD} from '../interceptors/csrf-interceptor';
+import { ADD_CSRF_TO_FORMDATA, ADD_CSRF_TO_PAYLOAD } from '../interceptors/csrf-interceptor';
 
 type HttpOptions = {
   csrf?: boolean;
   csrfInFormData?: boolean;
-  isForm?: boolean;
-  isFormData?: boolean;
+  isForm?: boolean; // For URL-encoded form data
+  isFormData?: boolean; // For multipart/form-data (e.g., file uploads)
   headers?: { [key: string]: string };
   params?: { [key: string]: string | number | boolean };
 };
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/storage.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/storage.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/storage.ts	(working copy)
@@ -1,7 +1,7 @@
 import {Injectable} from '@angular/core';
 
 @Injectable({
-  providedIn: 'root'
+  providedIn: 'root',
 })
 export class Storage {
 
Index: /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/system-service.ts
===================================================================
--- /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/system-service.ts	(revision 2676)
+++ /branches/amp_4_0/src/webui/webui/htdocs/new/src/gui/src/app/services/system-service.ts	(working copy)
@@ -426,6 +426,16 @@
       csrf: true,
       isFormData: true,
       csrfInFormData: true
+    })
+  }
+
+  upload(queryParam: string | null = null, payload: any) {
+    let baseURL: string = URLS.COMMON_UPLOAD_URL;
+    if (queryParam !== null) {
+      baseURL += `?${queryParam}`;
+    }
+    return this.http.post(baseURL, payload, {
+      isFormData: true
     })
   }
 }
