feat(multi load): submitting multiple found tables at once

This commit is contained in:
Mihajlo Medjedovic 2024-06-18 00:37:41 +02:00
parent 0a8b1e764c
commit 5deba44d2b
6 changed files with 345 additions and 78 deletions

View File

@ -0,0 +1,34 @@
import { BaseSASResponse } from './common/BaseSASResponse'
export interface EditorsStageDataSASResponse extends BaseSASResponse {
SYSDATE: string;
SYSTIME: string;
sasparams: Sasparam[];
_DEBUG: string;
_PROGRAM: string;
AUTOEXEC: string;
MF_GETUSER: string;
SYSCC: string;
SYSENCODING: string;
SYSERRORTEXT: string;
SYSHOSTINFOLONG: string;
SYSHOSTNAME: string;
SYSPROCESSID: string;
SYSPROCESSMODE: string;
SYSPROCESSNAME: string;
SYSJOBID: string;
SYSSCPL: string;
SYSSITE: string;
SYSTCPIPHOSTNAME: string;
SYSUSERID: string;
SYSVLONG: string;
SYSWARNINGTEXT: string;
END_DTTM: string;
MEMSIZE: string;
}
export interface Sasparam {
STATUS: string | 'SUCCESS';
DSID: string;
URL: string;
}

View File

@ -3,7 +3,7 @@
<clr-spinner clrMedium></clr-spinner>
</div>
<div class="text-center mb-10">
<div *ngIf="!parsedDatasets.length" class="text-center mb-10">
<button
(click)="fileUploadInput.click()"
class="btn btn-primary btn-sm"
@ -21,13 +21,28 @@
/>
</div>
<ng-container *ngIf="parsedDatasets.length > 0">
<p cds-text="caption" class="ml-10">Found tables:</p>
<ng-container *ngIf="parsedDatasets.length && !submittedDatasets.length">
<div class="text-center mb-10">
<button
(click)="onDiscard()"
class="btn btn-danger btn-sm mr-10"
>
Discard
</button>
<button
(click)="onSubmitAll()"
class="btn btn-primary btn-sm"
>
Submit All
</button>
</div>
<p cds-text="caption" class="ml-10 mb-10">Found tables:</p>
<clr-tree>
<clr-tree-node *ngFor="let dataset of parsedDatasets">
<button
(click)="onParsedDatasetClick(dataset)"
class="clr-treenode-link"
class="clr-treenode-link whitespace-nowrap"
[class.active]="dataset.active"
>
<cds-icon
@ -47,6 +62,32 @@
</clr-tree>
</ng-container>
<ng-container *ngIf="submittedDatasets.length">
<p cds-text="caption" class="ml-10 mb-10 mt-10">Submitted tables:</p>
<clr-tree>
<clr-tree-node *ngFor="let dataset of submittedDatasets">
<button
(click)="onSubmittedDatasetClick(dataset)"
class="clr-treenode-link whitespace-nowrap"
[class.active]="dataset.active"
>
<cds-icon
*ngIf="dataset.error"
status="danger"
shape="exclamation-circle"
></cds-icon>
<cds-icon
*ngIf="dataset.success"
status="success"
shape="check-circle"
></cds-icon>
<cds-icon shape="file"></cds-icon>
{{ dataset.libds }}
</button>
</clr-tree-node>
</clr-tree>
</ng-container>
<!-- <div *ngIf="librariesPaging" class="w-100 text-center">
<span class="spinner spinner-sm"> Loading... </span>
</div> -->
@ -146,72 +187,59 @@
class="is-info icon-dc-fill"
></clr-icon>
<p class="text-center color-gray mt-10" cds-text="section">
Please select a dataset on the left
Please select a dataset on the left to {{ !submittedDatasets.length ? 'review data' : 'review submitted results' }}
</p>
</div>
<div class="d-flex clr-justify-content-between p-10">
<div>
<p cds-text="secondary regular" class="mb-10">
Found in range:
<strong
>"{{
activeParsedDataset?.parseResult?.rangeSheetRes?.sheetName
}}"!{{
activeParsedDataset?.parseResult?.rangeSheetRes?.rangeAddress
}}</strong
>
</p>
<p cds-text="secondary regular">
Matched with dataset: <strong>LIB1.MPE_X_DATA</strong>
</p>
<ng-container *ngIf="activeParsedDataset">
<div class="d-flex clr-justify-content-between p-10">
<div>
<p cds-text="secondary regular" class="mb-10">
Found in range:
<strong
>"{{
activeParsedDataset.parseResult.rangeSheetRes?.sheetName
}}"!{{
activeParsedDataset.parseResult.rangeSheetRes?.rangeAddress
}}</strong
>
</p>
<p cds-text="secondary regular">
Matched with dataset: <strong>{{ activeParsedDataset.libds }}</strong>
</p>
</div>
<div>
<clr-toggle-wrapper>
<input
type="checkbox"
clrToggle
[(ngModel)]="activeParsedDataset.includeInSubmission"
name="options"
required
value="option1"
/>
<label>Include in submission</label>
</clr-toggle-wrapper>
</div>
</div>
<div>
<clr-toggle-wrapper>
<input
type="checkbox"
clrToggle
name="options"
required
value="option1"
/>
<label>Include in submission</label>
</clr-toggle-wrapper>
</div>
</div>
<hot-table
hotId="hotInstance"
id="hotTable"
class="mt-15"
className="htDark"
[licenseKey]="hotTableLicenseKey"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[manualColumnResize]="true"
[filters]="true"
stretchH="all"
>
</hot-table>
</ng-container>
</ng-container>
<!--
<div *ngIf="!noData && !noDataReqErr && table" class="clr-flex-1">
<hot-table
hotId="hotInstance"
id="hotTable"
className="htDark"
[multiColumnSorting]="true"
[viewportRowRenderingOffset]="50"
[data]="hotTable.data"
[colHeaders]="hotTable.colHeaders"
[columns]="hotTable.columns"
[copyPaste]="hotTable.copyPaste"
[contextMenu]="hotTable.contextMenu"
[filters]="true"
[dropdownMenu]="hotTable.dropdownMenu"
[height]="hotTable.height"
stretchH="all"
[modifyColWidth]="maxWidthCheker"
[cells]="hotTable.cells"
[maxRows]="hotTable.maxRows"
[manualColumnResize]="true"
[afterGetColHeader]="hotTable.afterGetColHeader"
[rowHeaders]="hotTable.rowHeaders"
[rowHeaderWidth]="hotTable.rowHeaderWidth"
[rowHeights]="hotTable.rowHeights"
[licenseKey]="hotTable.licenseKey"
>
</hot-table>
</div> -->
<!-- <div>
<p
*ngIf="
@ -227,3 +255,30 @@
</div> -->
</div>
</div>
<clr-modal [(clrModalOpen)]="showSubmitReasonModal">
<h3 class="modal-title">Submit for approval {{parsedDatasets.length}} tables</h3>
<div class="modal-body">
<div class="text-area-full-width">
<label for="formFields_8" class="mb-5 d-block"
>Message</label
>
<textarea
clrTextarea
[(ngModel)]="submitReasonMessage"
tabindex="0"
class="submit-reason"
type="text"
id="formFields_8"
></textarea>
</div>
<p cds-text="caption_clean" class="mt-10">
Sheets which did not match any dataset will be ignored. Tables will be sent sequentially, logs will be available after all tables are submitted.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" [disabled]="submitLoading" (click)="showSubmitReasonModal = false">Cancel</button>
<button type="button" class="btn btn-primary" [clrLoading]="submitLoading" (click)="submitTables()">Submit</button>
</div>
</clr-modal>

View File

@ -24,4 +24,10 @@
min-height: 200px;
height: 200px;
}
}
.submit-reason {
min-height: 70px;
max-height: 70px;
height: 70px;
}

View File

@ -1,11 +1,8 @@
import { Component, HostBinding, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ChangeDetectorRef, Component, HostBinding, OnInit, SimpleChanges } from '@angular/core'
import {
EventService,
HelperService,
LicenceService,
LoggerService,
SasService,
SasStoreService
} from '../services'
import * as XLSX from '@sheet/crypto'
@ -19,6 +16,10 @@ import {
ParseResult,
SpreadsheetService
} from '../services/spreadsheet.service'
import Handsontable from 'handsontable'
import { HotTableRegisterer } from '@handsontable/angular'
import { Router } from '@angular/router'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
@Component({
selector: 'app-multi-dataset',
@ -36,25 +37,35 @@ export class MultiDatasetComponent implements OnInit {
public selectedFile: File | null = null
public parsedDatasets: ParsedDataset[] = []
public submittedDatasets: SubmittedDatasetResult[] = []
public datasetsLoading: boolean = false
public uploadLoading: boolean = false
public submitLoading: boolean = false
public matchedDatasets: string[] = []
public userInputDatasets: string = ''
public uploadLoading: boolean = false
public libsAndTables: {
[key: string]: string[]
} = {}
public hotInstance!: Handsontable
private hotRegisterer: HotTableRegisterer
public showSubmitReasonModal: boolean = false
public submitReasonMessage: string = ''
constructor(
private eventService: EventService,
private licenceService: LicenceService,
private helperService: HelperService,
private sasStoreService: SasStoreService,
private spreadsheetService: SpreadsheetService
) {}
private spreadsheetService: SpreadsheetService,
private cdr: ChangeDetectorRef
) {
this.hotRegisterer = new HotTableRegisterer()
}
ngOnInit() {
this.licenceService.hot_license_key.subscribe(
@ -154,9 +165,28 @@ export class MultiDatasetComponent implements OnInit {
console.log('parseResult', parseResult)
if (parseResult && parseResult.data) {
let datasource: any[] = []
parseResult.data.map((item) => {
let itemObject: any = {}
parseResult.headerShow!.map((header: any, index: number) => {
itemObject[header] = item[index]
})
// If Delete? column is not set in the file, we set it to NO
if (!itemObject['_____DELETE__THIS__RECORD_____'])
itemObject['_____DELETE__THIS__RECORD_____'] = 'No'
datasource.push(itemObject)
})
this.parsedDatasets.push({
libds: datasetObject.libds,
parseResult: parseResult
parseResult: parseResult,
includeInSubmission: true,
datasetInfo: datasetObject,
datasource: datasource
})
}
})
@ -167,6 +197,34 @@ export class MultiDatasetComponent implements OnInit {
})
}
onSubmitAll() {
this.showSubmitReasonModal = true
}
onDiscard() {
this.parsedDatasets = []
this.matchedDatasets = []
this.selectedFile = null
this.userInputDatasets = ''
this.submitReasonMessage = ''
}
initHot() {
setTimeout(() => {
if (!this.hotInstance) this.hotInstance = this.hotRegisterer.getInstance('hotInstance')
if (this.activeParsedDataset) {
this.hotInstance.updateSettings({
data: this.activeParsedDataset.datasetInfo.data.sasdata,
colHeaders: this.activeParsedDataset.datasetInfo.headerColumns,
columns: this.activeParsedDataset.datasetInfo.dcValidator?.getRules(),
readOnly: true,
height: '300px',
})
}
})
}
onUserInputDatasetsChange() {
this.helperService.debounceCall(500, () => {
const inputDatasets = this.userInputDatasets.split('\n')
@ -227,6 +285,14 @@ export class MultiDatasetComponent implements OnInit {
this.deselectAllParsedDatasets()
parsedDataset.active = true
this.cdr.detectChanges()
this.initHot()
}
onSubmittedDatasetClick(submittedDataset: SubmittedDatasetResult) {
}
public get activeParsedDataset(): ParsedDataset | undefined {
@ -260,6 +326,89 @@ export class MultiDatasetComponent implements OnInit {
return undefined
}
/**
* Sends tables to the SAS sequentially
*/
async submitTables() {
console.info('Submitting multiple tables', this.parsedDatasets)
this.submitLoading = true
let requestsResults: SubmittedDatasetResult[] = []
for (let table of this.parsedDatasets) {
// Skip the table if toggle switch is off
if (!table.includeInSubmission) continue
let updateParams: any = {}
this.submitReasonMessage = this.submitReasonMessage.replace(/\n/g, '. ')
updateParams.ACTION = 'LOAD'
updateParams.MESSAGE = this.submitReasonMessage
updateParams.LIBDS = table.libds
let data = table.datasource
if (data) {
data = data.map((row: any) => {
let deleteColValue = row['_____DELETE__THIS__RECORD_____']
delete row['_____DELETE__THIS__RECORD_____']
row['_____DELETE__THIS__RECORD_____'] = deleteColValue
// If cell is numeric and value is dot `.` we change it to `null`
Object.keys(row).map((key: string) => {
const colRule = table.datasetInfo.dcValidator?.getRule(key)
if (colRule?.type === 'numeric' && row[key] === '.') row[key] = null
})
return row
})
const submitData = data.slice(
0,
this.licenceState.value.submit_rows_limit
)
console.log('submitData', submitData)
let error
let success
await this.sasStoreService
.updateTable(
updateParams,
table.datasource,
'SASControlTable',
'editors/stagedata',
table.datasetInfo.data.$sasdata
)
.then((res: EditorsStageDataSASResponse) => {
success = res
})
.catch((err: any) => {
console.error('err', err)
error = err
})
requestsResults.push({
success,
error,
libds: table.libds
})
}
}
console.log('requestsResults', requestsResults)
this.submittedDatasets = requestsResults
this.showSubmitReasonModal = false
this.submitLoading = false
this.deselectAllParsedDatasets()
}
private parseExcelSheetNames(): Promise<string[]> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
@ -439,7 +588,17 @@ export interface DatasetsObject extends EditorsGetDataServiceResponse {
export interface ParsedDataset {
libds: string
parseResult: ParseResult,
datasetInfo: DatasetsObject,
datasource: any[],
includeInSubmission: boolean
status?: 'success' | 'error'
active?: boolean
parseResult: ParseResult
}
export interface SubmittedDatasetResult {
libds: string,
success: EditorsStageDataSASResponse | undefined,
error: any,
active?: boolean
}

View File

@ -17,10 +17,10 @@ import { LoggerService } from './logger.service'
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import { Col } from '../shared/dc-validator/models/col.model'
import { get } from 'lodash-es'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
@Injectable()
export class SasStoreService {
public libds!: string
public response: Subject<any> = new Subject<any>()
public changedTable: Subject<any> = new Subject<any>()
public details: Subject<any> = new Subject<any>()
@ -56,7 +56,6 @@ export class SasStoreService {
program: string,
libds: string
) {
this.libds = libds
const tables: any = {}
tables[tableName] = [tableData]
const res: EditorsGetDataSASResponse = await this.sasService.request(
@ -65,7 +64,7 @@ export class SasStoreService {
)
const response: EditorsGetDataServiceResponse = {
data: res,
libds: this.libds
libds: libds
}
return response
}
@ -85,7 +84,7 @@ export class SasStoreService {
tableName: string,
program: string,
$dataFormats: $DataFormats | null
) {
): Promise<EditorsStageDataSASResponse> {
// add sp as third argument of createData call
let tables: any = {

View File

@ -84,6 +84,17 @@ body[cds-theme="light"] {
line-height: 1.8 !important;
}
[cds-text=caption_clean] {
font-size: var(--cds-global-typography-caption-font-size);
font-weight: var(--cds-global-typography-caption-font-weight);
line-height: var(--cds-global-typography-caption-line-height);
letter-spacing: var(--cds-global-typography-caption-letter-spacing);
&::after, &::before {
display: none;
}
}
// Custom loading spinner
.slider {
position: absolute;
@ -466,6 +477,10 @@ body[cds-theme="light"] {
pointer-events: none;
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-center {
text-align: center;
}
@ -1048,7 +1063,6 @@ clr-tree-node {
padding: 0px 8px 0px 8px;
width: auto;
height: auto;
display: flex;
align-items: center;
}