aggregation

This commit is contained in:
string 2025-10-18 18:22:00 +05:30
parent 7f80ea6330
commit 56e1e3b0fe
3 changed files with 589 additions and 140 deletions

View File

@ -17,8 +17,9 @@
</button>
</div>
</div>
<ng-container *ngIf="!isCardview"> <!-- GET ALL --> <clr-datagrid [clrDgLoading]="loading"
[(clrDgSelected)]="selected">
<ng-container *ngIf="!isCardview"> <!-- GET ALL -->
<div style="overflow-x: auto;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selected" style="width: max-content; min-width: 100%;">
<clr-dg-placeholder>
<ng-template #loadingSpinner>
<clr-spinner>Loading ... </clr-spinner>
@ -33,6 +34,8 @@
<clr-dg-column [clrDgField]="'url'"> <ng-container *clrDgHideableColumn="{hidden: false}"> Url
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'url_endpoint'"> <ng-container *clrDgHideableColumn="{hidden: false}"> Url Endpoint
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'schedule'"> <ng-container *clrDgHideableColumn="{hidden: false}"> schedule
</ng-container></clr-dg-column>
@ -45,20 +48,26 @@
<clr-dg-column [clrDgField]="'json'"> <ng-container *clrDgHideableColumn="{hidden: false}"> json
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'url_endpoint'"> <ng-container *clrDgHideableColumn="{hidden: false}"> Url Endpoint
<!-- Add groupby_json column -->
<clr-dg-column [clrDgField]="'groupby_json'"> <ng-container *clrDgHideableColumn="{hidden: false}">
Group By JSON
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'batch_volume'"> <ng-container *clrDgHideableColumn="{hidden: false}"> Batch Volume
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'sure_connect_id'"> <ng-container *clrDgHideableColumn="{hidden: false}"> SureConnect
<clr-dg-column [clrDgField]="'sure_connect_id'"> <ng-container *clrDgHideableColumn="{hidden: false}">
SureConnect
</ng-container></clr-dg-column>
<clr-dg-column [clrDgField]="'calculated_field_json'"> <ng-container *clrDgHideableColumn="{hidden: false}">
Calculated Fields
</ng-container></clr-dg-column>
<!-- who column -->
<clr-dg-column>
<ng-container *clrDgHideableColumn="{hidden: false}">
@ -71,20 +80,6 @@
<clr-dg-cell>{{user.name }}</clr-dg-cell>
<clr-dg-cell (click)="goTourlUrl(user.url)"
style="cursor: pointer; color: rgb(108, 108, 194);">{{user.url}}</clr-dg-cell>
<clr-dg-cell>{{user.schedule }}</clr-dg-cell>
<clr-dg-cell>{{user.cron_job }}</clr-dg-cell>
<clr-dg-cell (click)="goToReplaceStringjson(user.json)" style="cursor: pointer; align-items: center;"><clr-icon
shape="details"></clr-icon></clr-dg-cell>
<clr-dg-cell>
<div style="display: flex; align-items: center; max-width: 200px;">
<span
@ -97,6 +92,36 @@
</button>
</div>
</clr-dg-cell>
<clr-dg-cell>
<div style="display: flex; align-items: center; max-width: 200px;">
<span
style="margin-right: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1;"
title="{{user.url_endpoint}}" (click)="showFullUrl(user.url_endpoint)">
{{user.url | slice:0:30}}{{user.url_endpoint.length > 30 ? '...' : ''}}
</span>
<button class="btn btn-icon btn-sm" (click)="copyToClipboard(user.url_endpoint)" title="Copy URL">
<clr-icon shape="copy-to-clipboard"></clr-icon>
</button>
</div>
</clr-dg-cell>
<clr-dg-cell>{{user.schedule }}</clr-dg-cell>
<clr-dg-cell>{{user.cron_job }}</clr-dg-cell>
<clr-dg-cell (click)="goToReplaceStringjson(user.json)"
style="cursor: pointer; align-items: center;"><clr-icon shape="details"></clr-icon></clr-dg-cell>
<!-- Add groupby_json cell -->
<clr-dg-cell (click)="goToReplaceStringjson(user.groupby_json)"
style="cursor: pointer; align-items: center;"><clr-icon shape="details"
*ngIf="user.groupby_json"></clr-icon></clr-dg-cell>
<clr-dg-cell>{{user.batch_volume}}</clr-dg-cell>
<clr-dg-cell>{{user.sureconnect_name}}</clr-dg-cell>
@ -105,8 +130,11 @@
style="cursor: pointer; align-items: center;"><clr-icon shape="details"
*ngIf="user.calculated_field_json"></clr-icon></clr-dg-cell>
<!-- who column -->
<clr-dg-cell>
<div style="display: flex; align-items: center; gap: 1px; flex-wrap: nowrap;">
<clr-signpost>
<span style="cursor: pointer;" clrSignpostTrigger><clr-icon shape="help" class="success"
style="color: rgb(0, 130, 236);"></clr-icon></span>
@ -121,14 +149,20 @@
</clr-signpost>
<!-- New JSON Update button -->
<button class="btn btn-icon" (click)="updateJson(user.id)" title="Update JSON">
<button class="btn btn-icon btn-sm" (click)="updateJson(user.id)" title="Update JSON">
<clr-icon shape="refresh"></clr-icon>
</button>
<!-- Calculated Field button -->
<button class="btn btn-icon" (click)="fetchAvailableKeys(user)" title="Create Calculated Field">
<button class="btn btn-icon btn-sm" (click)="fetchAvailableKeys(user)" title="Create Calculated Field">
<clr-icon shape="calculator"></clr-icon>
</button>
<!-- Group By button -->
<button class="btn btn-icon btn-sm" (click)="openGroupByModal(user)" title="Group By Configuration">
<clr-icon shape="group"></clr-icon>
</button>
</div>
</clr-dg-cell>
<!-- who colmn -->
@ -155,7 +189,9 @@
of {{pagination.totalItems}} users
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid> </ng-container>
</clr-datagrid>
</div>
</ng-container>
<ng-template #showInfo>
<div class="alert alert-info" role="alert">
<div class="alert-items">
@ -289,7 +325,41 @@
<clr-modal [(clrModalOpen)]="rsModaljson" [clrModalSize]="'xl'" [clrModalStaticBackdrop]="true">
<div class="modal-body">
<textarea class="form-control" style="width:100%; height: 400px;" readonly>{{rowSelected}}</textarea>
<!-- Grid view above JSON -->
<div *ngIf="isJsonData(rowSelected)" class="json-grid-view" style="margin-bottom: 20px;">
<h4>JSON Data Grid View</h4>
<div style="overflow-x: auto; max-height: 300px; overflow-y: auto;">
<clr-datagrid style="width: max-content; min-width: 100%;">
<!-- Generate columns dynamically based on JSON keys -->
<clr-dg-column *ngFor="let header of getJsonHeaders(rowSelected)" style="min-width: 150px;">
<ng-container *clrDgHideableColumn="{hidden: false}">
{{header}}
</ng-container>
</clr-dg-column>
<!-- Generate rows dynamically from JSON data -->
<clr-dg-row *ngFor="let item of getJsonData(rowSelected); let i = index" [clrDgItem]="item">
<clr-dg-cell *ngFor="let header of getJsonHeaders(rowSelected)" style="min-width: 150px;">
<span [title]="item[header]"
style="display: inline-block; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{item[header]}}
</span>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #jsonPagination [clrDgPageSize]="5">
<clr-dg-page-size [clrPageSizeOptions]="[5, 10, 20]">Items per page</clr-dg-page-size>
{{jsonPagination.firstItem + 1}} - {{jsonPagination.lastItem + 1}}
of {{getJsonData(rowSelected).length}} items
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<!-- JSON view below grid -->
<textarea class="form-control" style="width:100%; height: 200px;" readonly>{{rowSelected}}</textarea>
</div>
</clr-modal>
@ -641,11 +711,12 @@
</select>
</div>
<div class="constant-input-container">
<input type="text" class="constant-input" formControlName="constant" placeholder="Or enter constant value" />
<input type="text" class="constant-input" formControlName="constant"
placeholder="Or enter constant value" />
</div>
<div class="remove-btn-container">
<button type="button" class="btn btn-icon btn-danger remove-field-btn" (click)="removeFieldComponent(i)"
*ngIf="getFieldComponents().length > 1">
<button type="button" class="btn btn-icon btn-danger remove-field-btn"
(click)="removeFieldComponent(i)" *ngIf="getFieldComponents().length > 1">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
@ -681,7 +752,7 @@
<td class="operation-cell">{{ field.operation }}</td>
<td class="expression-cell">
<span *ngIf="field.operation === 'complex'">{{ field.complexEquation }}</span>
<span *ngIf="field.operation !== 'complex'">
<span *ngIf="field.operation !== 'complex' && field.type !== 'groupby'">
<span *ngFor="let component of field.fieldComponents; let last = last">
<span *ngIf="component.field && !component.isConstant">{{ component.field }}</span>
<span *ngIf="component.isConstant">"{{ component.constant }}"</span>
@ -689,9 +760,24 @@
<span *ngIf="!last"> {{ getOperationSymbol(field.operation) }} </span>
</span>
</span>
<span *ngIf="field.type === 'groupby'">
GROUP BY: {{ field.groupFields.join(', ') }}
<div *ngIf="field.aggregations && field.aggregations.length > 0">
<small>
<div *ngFor="let agg of field.aggregations">
{{ agg.operation.toUpperCase() }}({{ agg.field }})
</div>
</small>
</div>
</span>
</td>
<td class="actions-cell">
<button class="btn btn-icon btn-danger delete-field-btn" (click)="deleteCalculatedField(field.id)">
<td>
<button class="btn btn-icon btn-danger" (click)="deleteCalculatedField(field.id)"
*ngIf="field.type !== 'groupby'">
<clr-icon shape="trash"></clr-icon>
</button>
<button class="btn btn-icon btn-danger" (click)="removeGroupByConfig(field.id)"
*ngIf="field.type === 'groupby'">
<clr-icon shape="trash"></clr-icon>
</button>
</td>
@ -732,15 +818,76 @@
</div>
</clr-modal>
<!-- Group By Modal -->
<clr-modal [(clrModalOpen)]="showGroupByModal" [clrModalSize]="'lg'" [clrModalStaticBackdrop]="true">
<h3 class="modal-title">Group By Configuration</h3>
<div class="modal-body">
<div class="clr-row">
<div class="clr-col-12">
<h4>Group By Fields</h4>
<div class="field-container">
<span *ngFor="let key of availableKeys" class="field-tag" [class.selected]="isGroupByFieldSelected(key)"
(click)="isGroupByFieldSelected(key) ? removeGroupByField(key) : addGroupByField(key)">
{{ key }}
</span>
</div>
</div>
<div class="clr-col-12" style="margin-top: 20px;">
<h4>Selected Group By Fields</h4>
<div class="selected-fields-container">
<span *ngFor="let field of selectedGroupByFields" class="selected-field-tag">
{{ field }}
<button class="btn btn-icon btn-sm" (click)="removeGroupByField(field)">
<clr-icon shape="times"></clr-icon>
</button>
</span>
<div *ngIf="selectedGroupByFields.length === 0" class="no-selection">
No fields selected for grouping
</div>
</div>
</div>
<div class="clr-col-12" style="margin-top: 20px;">
<h4>Aggregation Operations</h4>
<div class="aggregation-container">
<div *ngFor="let agg of selectedAggregationFields; let i = index" class="aggregation-row">
<div class="clr-row">
<div class="clr-col-5">
<label>Field</label>
<select class="clr-select" [(ngModel)]="agg.field">
<option value="">Select Field</option>
<option *ngFor="let key of availableKeys" [value]="key">{{ key }}</option>
</select>
</div>
<div class="clr-col-5">
<label>Operation</label>
<select class="clr-select" [(ngModel)]="agg.operation">
<option *ngFor="let op of aggregationOperations" [value]="op.value">{{ op.label }}</option>
</select>
</div>
<div class="clr-col-2">
<button class="btn btn-icon btn-danger" (click)="removeAggregationField(i)" style="margin-top: 20px;">
<clr-icon shape="trash"></clr-icon>
</button>
</div>
</div>
</div>
<button class="btn btn-sm" (click)="addAggregationField()" style="margin-top: 10px;">
<clr-icon shape="plus"></clr-icon> Add Aggregation
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="showGroupByModal = false">Cancel</button>
<button type="button" class="btn btn-primary" (click)="applyGroupBy()"
[disabled]="selectedGroupByFields.length === 0">
Apply Group By
</button>
</div>
</clr-modal>
<!-- htmlpopup -->

View File

@ -295,3 +295,92 @@
margin-bottom: 8px;
}
}
/* Group By Modal Styles */
.field-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.field-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;
}
.field-tag:hover {
background-color: #bbdefb;
transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.field-tag.selected {
background-color: #1976d2;
color: white;
border-color: #1976d2;
}
.selected-fields-container {
min-height: 50px;
padding: 10px;
border: 1px dashed #ccc;
border-radius: 4px;
background-color: #f9f9f9;
}
.selected-field-tag {
display: inline-flex;
align-items: center;
background-color: #1976d2;
color: white;
border-radius: 20px;
padding: 6px 12px;
margin: 4px;
font-size: 13px;
}
.selected-field-tag .btn-icon {
margin-left: 8px;
padding: 2px;
color: white;
}
.no-selection {
color: #999;
font-style: italic;
padding: 10px;
}
.aggregation-container {
margin-top: 10px;
}
.aggregation-row {
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
}
.aggregation-row label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.clr-select {
width: 100%;
padding: 5px;
border: 1px solid #ccc;
border-radius: 4px;
}

View File

@ -76,6 +76,25 @@ export class Data_lakeComponent implements OnInit {
currentConstantValue = '';
pendingEquationUpdate = '';
// New properties for group by functionality
groupByFields: string[] = [];
selectedGroupByFields: string[] = [];
showGroupByModal = false;
// Aggregation operations
aggregationOperations = [
{ value: 'count', label: 'Count' },
{ value: 'sum', label: 'Sum' },
{ value: 'avg', label: 'Average' },
{ value: 'min', label: 'Minimum' },
{ value: 'max', label: 'Maximum' },
{ value: 'median', label: 'Median' },
{ value: 'mode', label: 'Mode' },
{ value: 'stdev', label: 'Standard Deviation' }
];
selectedAggregationFields: { field: string; operation: string }[] = [];
constructor(
private extensionService: ExtensionService,
private userInfoService: UserInfoService,
@ -745,6 +764,7 @@ export class Data_lakeComponent implements OnInit {
this.ngOnInit();
}, 500);
}
goToAdd(row) {
this.modalAdd = true;
this.addCronExpression = '';
@ -807,8 +827,10 @@ export class Data_lakeComponent implements OnInit {
this.product[index].iscalculatedfield = true;
}
// Close the modal
// Close the modal only when called directly from the UI
if (this.calculatedFieldModalOpen) {
this.calculatedFieldModalOpen = false;
}
},
(error) => {
console.error('Error updating calculated fields:', error);
@ -968,4 +990,195 @@ export class Data_lakeComponent implements OnInit {
console.log('Validation result:', isValid);
console.log('Defined constants:', this.getDefinedConstants());
}
// Method to open group by modal
openGroupByModal(dataLakeItem: any) {
this.selectedDataLakeItem = dataLakeItem;
// Fetch available keys if not already fetched
if (this.availableKeys.length === 0) {
this.fetchAvailableKeys(dataLakeItem);
}
// Initialize selected group by fields as empty array
this.selectedGroupByFields = [];
this.selectedAggregationFields = [];
this.showGroupByModal = true;
}
// Method to add a field to group by selection
addGroupByField(field: string) {
if (!this.selectedGroupByFields.includes(field)) {
this.selectedGroupByFields.push(field);
}
}
// Method to remove a field from group by selection
removeGroupByField(field: string) {
const index = this.selectedGroupByFields.indexOf(field);
if (index > -1) {
this.selectedGroupByFields.splice(index, 1);
}
}
// Method to add an aggregation field
addAggregationField() {
this.selectedAggregationFields.push({ field: '', operation: 'count' });
}
// Method to remove an aggregation field
removeAggregationField(index: number) {
this.selectedAggregationFields.splice(index, 1);
}
// Method to update aggregation field
updateAggregationField(index: number, field: string, operation: string) {
this.selectedAggregationFields[index] = { field, operation };
}
// Method to apply group by
applyGroupBy() {
if (this.selectedGroupByFields.length === 0) {
this.toastr.error('Please select at least one field for grouping');
return;
}
// Validate aggregation fields
for (const agg of this.selectedAggregationFields) {
if (!agg.field) {
this.toastr.error('Please select a field for all aggregation operations');
return;
}
}
// Create group by configuration
const groupByConfig = {
id: Date.now(),
type: 'groupby',
groupFields: [...this.selectedGroupByFields],
aggregations: [...this.selectedAggregationFields],
timestamp: new Date().toISOString()
};
// Add to calculated fields
this.calculatedFields.push(groupByConfig);
this.toastr.success('Group by configuration added successfully');
// Update the calculated fields in the backend
this.updateCalculatedFieldsAfterGroupBy();
this.showGroupByModal = false;
}
// New method to update calculated fields after adding group by configuration
updateCalculatedFieldsAfterGroupBy() {
if (!this.selectedDataLakeItem || this.calculatedFields.length === 0) {
return;
}
// Convert calculated fields to JSON string
const calculatedFieldJson = JSON.stringify(this.calculatedFields);
// Call the service method to update the record
this.mainService.updateCalculatedFields(
this.selectedDataLakeItem.id,
calculatedFieldJson,
true // iscalculatedfield = true
).subscribe(
(response) => {
console.log('Calculated fields updated successfully:', response);
this.toastr.success('Group by configuration saved successfully');
// Update the local data
const index = this.product.findIndex(item => item.id === this.selectedDataLakeItem.id);
if (index !== -1) {
this.product[index].calculated_field_json = calculatedFieldJson;
this.product[index].iscalculatedfield = true;
}
},
(error) => {
console.error('Error updating calculated fields:', error);
this.toastr.error('Failed to save group by configuration');
}
);
}
// Method to remove group by configuration
removeGroupByConfig(id: number) {
this.calculatedFields = this.calculatedFields.filter(field => field.id !== id || field.type !== 'groupby');
this.toastr.success('Group by configuration removed');
}
// Method to check if a field is selected for group by
isGroupByFieldSelected(field: string): boolean {
return this.selectedGroupByFields.includes(field);
}
// Helper method to get the count of calculated fields
getCalculatedFieldsCount(calculatedFieldJson: string): number {
try {
const fields = JSON.parse(calculatedFieldJson);
return Array.isArray(fields) ? fields.length : 0;
} catch (e) {
return 0;
}
}
// Helper method to get the count of group by configurations
getGroupByFieldsCount(groupByJson: string): number {
try {
const configs = JSON.parse(groupByJson);
return Array.isArray(configs) ? configs.length : 0;
} catch (e) {
return 0;
}
}
// Helper method to check if the selected data is JSON
isJsonData(data: any): boolean {
if (!data) return false;
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
return Array.isArray(parsed) && parsed.length > 0;
} catch (e) {
return false;
}
}
// Helper method to get JSON headers for grid columns
getJsonHeaders(data: any): string[] {
if (!this.isJsonData(data)) return [];
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
if (Array.isArray(parsed) && parsed.length > 0) {
// Get all unique keys from the first few items
const headers = new Set<string>();
const sampleItems = parsed.slice(0, 3); // Check first 3 items for headers
sampleItems.forEach(item => {
if (typeof item === 'object' && item !== null) {
Object.keys(item).forEach(key => headers.add(key));
}
});
return Array.from(headers);
}
return [];
} catch (e) {
return [];
}
}
// Helper method to get JSON data for grid rows
getJsonData(data: any): any[] {
if (!this.isJsonData(data)) return [];
try {
return typeof data === 'string' ? JSON.parse(data) : data;
} catch (e) {
return [];
}
}
}