diff --git a/src/app/components/components.module.ts b/src/app/components/components.module.ts index fee6f6d5..1c9b8ae1 100644 --- a/src/app/components/components.module.ts +++ b/src/app/components/components.module.ts @@ -37,8 +37,9 @@ import {JsonTextComponent} from './data-view/json-text/json-text.component'; import {NgxJsonViewerModule} from 'ngx-json-viewer'; import {JsonEditorComponent} from './json/json-editor.component'; import {JsonElemComponent} from './json/json-elem/json-elem.component'; +import {ListPickerComponent} from './list-picker/list-picker.component'; +import {DragDropModule} from '@angular/cdk/drag-drop'; import {DataGraphComponent} from './data-view/data-graph/data-graph.component'; -import {MultipleSwitchPipe} from './data-view/multiple-switch.pipe'; import {DatesPipeModule} from './data-view/shared-module'; //import 'hammerjs'; @@ -49,6 +50,7 @@ import {DatesPipeModule} from './data-view/shared-module'; RouterModule, CommonModule, ChartsModule, + DragDropModule, TypeaheadModule.forRoot(), AppBreadcrumbModule.forRoot(), TreeModule, @@ -73,6 +75,7 @@ import {DatesPipeModule} from './data-view/shared-module'; InformationManagerComponent, RenderItemComponent, InputComponent, + ListPickerComponent, EditorComponent, JsonEditorComponent, DataViewComponent, @@ -99,10 +102,11 @@ import {DatesPipeModule} from './data-view/shared-module'; ToastComponent, InformationManagerComponent, InputComponent, + ListPickerComponent, JsonEditorComponent, EditorComponent, DeleteConfirmComponent ] }) export class ComponentsModule { -} +} \ No newline at end of file diff --git a/src/app/components/list-picker/list-picker.component.html b/src/app/components/list-picker/list-picker.component.html new file mode 100644 index 00000000..b2442409 --- /dev/null +++ b/src/app/components/list-picker/list-picker.component.html @@ -0,0 +1,21 @@ +
+ {{ sourceTitle }} +
+
+ {{ option[labelProperty] }} + +
+
+
+ +
+ {{ targetTitle }} +
+
+ + {{ option[labelProperty] }} +
+
+
\ No newline at end of file diff --git a/src/app/components/list-picker/list-picker.component.scss b/src/app/components/list-picker/list-picker.component.scss new file mode 100644 index 00000000..0c67e34c --- /dev/null +++ b/src/app/components/list-picker/list-picker.component.scss @@ -0,0 +1,66 @@ +.app-list-picker { + display: flex; + gap: 10px; + + .source-column, + .target-column { + display: flex; + flex-direction: column; + } + + .list-container { + min-width: 200px; + min-height: 200px; + max-height: 200px; + border: 1px #e4e7ea solid; + flex: 1 1 auto; + border-radius: 0.2rem; + overflow-y: scroll; + } + + .list-container .list-item:not(.cdk-drag-dragging) { + opacity: 1; + padding: 0.2rem 0.5rem; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: space-between; + border: 1px var(--white) solid; + border-radius: 0; + box-shadow: none; + border: 1x var(--white) solid; + background: var(--white); + + em { + color: var(--gray); + font-size: 10px; + } + } + + .list-container .list-item:not(.cdk-drag-dragging):hover { + background: var(--light); + cursor: pointer; + } + + .list-container .list-item.cdk-drag-placeholder { + opacity: 0.2; + } +} + +.cdk-drag.list-item { + opacity: 1; + padding: 0.2rem 0.5rem; + transition: all 0.1s; + display: flex; + align-items: center; + justify-content: space-between; + background: var(--light); + border: 1px var(--gray) solid; + border-radius: 0.2rem; + box-shadow: 0px 0px 5px var(--light); + + em { + color: var(--gray); + font-size: 10px; + } +} \ No newline at end of file diff --git a/src/app/components/list-picker/list-picker.component.ts b/src/app/components/list-picker/list-picker.component.ts new file mode 100644 index 00000000..3d234e7e --- /dev/null +++ b/src/app/components/list-picker/list-picker.component.ts @@ -0,0 +1,116 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { Component, EventEmitter, forwardRef, HostBinding, Input, Output, ViewEncapsulation } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * ListPickerComponent is used to select multiple values from a list of options. + */ +@Component({ + selector: 'app-list-picker', + templateUrl: './list-picker.component.html', + styleUrls: ['./list-picker.component.scss'], + encapsulation: ViewEncapsulation.None, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ListPickerComponent), + multi: true, + }, + ], +}) +export class ListPickerComponent implements ControlValueAccessor { + + @HostBinding("class") + classes = ["app-list-picker"] + + /** + * Title of the source options + */ + @Input() + sourceTitle = "Source"; + + /** + * Title of the target options + */ + @Input() + targetTitle = "Target"; + + /** + * An array of objects to display as the available options. + */ + @Input() + sourceOptions: Array + + /** + * Name of the label field of an option. + */ + @Input() + labelProperty: keyof T | null; + + /** + * Number of maximum options that can be selected. + */ + @Input() + selectionLimit = Infinity; + + /** + * Disable drag and drop sorting + */ + @Input() + disableSort = false; + + /** + * Callback to invoke when selection limit is reached. + */ + @Output() + selectionLimitReached = new EventEmitter(); + + _value: Array | null; + + _disabled: boolean; + + _onChange: (value: Array) => void + + _onTouched: () => void + + writeValue(value: Array): void { + this._value = value + } + + registerOnChange(fn: (value: Array) => void): void { + this._onChange = fn + } + + registerOnTouched(fn: () => void): void { + this._onTouched = fn + } + + setDisabledState?(isDisabled: boolean): void { + this._disabled = isDisabled; + } + + _getSourceOptions = () => { + return this.sourceOptions?.filter(option => !this._value?.includes(option)); + } + + _onAddOption = (option: T) => { + this._onTouched(); + if((this._value?.length ?? 0) < this.selectionLimit) { + this._value = [...this._value ?? [], option]; + this._onChange(this._value); + } else { + this.selectionLimitReached.emit() + } + } + + _onRemoveOption = (option: T) => { + this._value?.splice( this._value?.indexOf(option), 1); + this._onChange(this._value); + this._onTouched(); + } + + + _onMoveOption = (event: CdkDragDrop) => { + moveItemInArray(this._value, event.previousIndex, event.currentIndex); + } +} diff --git a/src/app/models/ui-request.model.ts b/src/app/models/ui-request.model.ts index 7b022c23..ea436410 100644 --- a/src/app/models/ui-request.model.ts +++ b/src/app/models/ui-request.model.ts @@ -230,6 +230,26 @@ export class ColumnRequest extends UIRequest { } } + +/** + * Merge columns within a relational namespace. + * Used for request where you want to merge multiple columns of a table. + */ +export class MergeColumnsRequest extends UIRequest { + sourceColumns: DbColumn[]; + targetColumnName: string; + joinString: string; + tableType: string; + constructor( tableId: string, sourceColumns: DbColumn[], targetColumnName: string, joinString: string, tableType:string = 'table' ) { + super(); + this.tableId = tableId; + this.sourceColumns = sourceColumns; + this.targetColumnName = targetColumnName; + this.joinString = joinString; + this.tableType = tableType; + } +} + export class MaterializedRequest extends UIRequest{ constructor(tableId: string) { super(); @@ -271,6 +291,24 @@ export class EditTableRequest { } } +/** + * Transfer a table from one namespace to another. + * Used for request where you want to transfer a table + */ + export class TransferTableRequest { + table: string; + sourceSchema: string; + targetSchema: string; + primaryKeyNames: string; + + constructor( table: string, sourceNamespaceName: string, targetNamespaceName: string, primaryKeyNames: string ) { + this.table = table; + this.sourceSchema = sourceNamespaceName; + this.targetSchema = targetNamespaceName; + this.primaryKeyNames = primaryKeyNames; + } +} + export class EditCollectionRequest { database: string; collection: string; diff --git a/src/app/services/crud.service.ts b/src/app/services/crud.service.ts index b9933953..9940416f 100644 --- a/src/app/services/crud.service.ts +++ b/src/app/services/crud.service.ts @@ -5,8 +5,7 @@ import { Index, ModifyPartitionRequest, PartitionFunctionModel, - PartitioningRequest, - ResultSet + PartitioningRequest } from '../components/data-view/models/result-set.model'; import {webSocket} from 'rxjs/webSocket'; import { @@ -18,7 +17,9 @@ import { ExploreTable, GraphRequest, MaterializedRequest, + MergeColumnsRequest, MonitoringRequest, + TransferTableRequest, QueryRequest, RelAlgRequest, Schema, @@ -204,6 +205,13 @@ export class CrudService { return this._http.post(`${this.httpUrl}/dropColumn`, columnRequest, this.httpOptions); } + /** + * Merge columns of a table in a relational namespace + */ + mergeColumns ( columnRequest: MergeColumnsRequest ) { + return this._http.post(`${this.httpUrl}/mergeColumns`, columnRequest, this.httpOptions); + } + /** * Get list of tables of a schema to truncate/drop them */ @@ -225,6 +233,13 @@ export class CrudService { return this._http.post(`${this.httpUrl}/createTable`, tableRequest, this.httpOptions); } + /** + * Transfer a table to another schema + */ + transferTable( tableRequest: TransferTableRequest ) { + return this._http.post(`${this.httpUrl}/transferTable`, tableRequest, this.httpOptions); + } + /** * Create a new collection */ @@ -618,4 +633,4 @@ export class CrudService { return 'is-invalid'; } } -} +} \ No newline at end of file diff --git a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.html b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.html index c2c7c175..903948f3 100644 --- a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.html +++ b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.html @@ -11,6 +11,7 @@
Collections in {{database}} [{{schemaType}}]
Collection Truncate Drop + Transfer @@ -50,6 +51,26 @@
Collections in {{database}} [{{schemaType}}]
+ +
+
+
+ + +
+ +
+
+ @@ -189,3 +210,37 @@ + + diff --git a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts index aa15c042..499f5827 100644 --- a/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts +++ b/src/app/views/schema-editing/document-edit-collections/document-edit-collections.component.ts @@ -1,6 +1,6 @@ import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {CrudService} from '../../../services/crud.service'; -import {EditCollectionRequest, EditTableRequest, SchemaRequest} from '../../../models/ui-request.model'; +import {EditCollectionRequest, EditTableRequest, SchemaRequest, TransferTableRequest} from '../../../models/ui-request.model'; import {ActivatedRoute, Router} from '@angular/router'; import {DbColumn, Index, PolyType, ResultSet, Status} from '../../../components/data-view/models/result-set.model'; import {ToastDuration, ToastService} from '../../../components/toast/toast.service'; @@ -15,13 +15,18 @@ import {UtilService} from '../../../services/util.service'; import * as $ from 'jquery'; import {DbTable} from '../../uml/uml.model'; +class Namespace { + name: string; + id: string; +} + @Component({ selector: 'app-document-edit-collections', templateUrl: './document-edit-collections.component.html', styleUrls: ['./document-edit-collections.component.scss'] }) export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { - + types: PolyType[] = []; database: string; schemaType: string; @@ -34,6 +39,11 @@ export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { selectedStore; creatingTable = false; + activeNamespace: string; + namespaces: Namespace[]; + selectedSchemas = new Map(); // name of the collection, name of the selected namespace + tableToTransfer : TableModel; + //export table showExportButton = false; exportProgress = 0.0; @@ -47,6 +57,11 @@ export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { addDefaultValue: new FormControl(true, Validators.required) }); @ViewChild('exportTableModal', {static: false}) public exportTableModal: ModalDirective; + transferTableForm = new FormGroup({ + name: new FormControl('', Validators.required), + }); + @ViewChild('transferTableModal', {static: false}) public transferTableModal: ModalDirective; + constructor( public _crud: CrudService, @@ -78,6 +93,7 @@ export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { }); this.subscriptions.add(sub2); this.documentListener(); + this.updateExistingSchemas(); } ngOnDestroy() { @@ -380,6 +396,72 @@ export class DocumentEditCollectionsComponent implements OnInit, OnDestroy { ); } + /** + * Transfer table (a collection) from one namepsace to another + */ + transferTable() { + let primaryKeyNames = this.transferTableForm.controls['name'].value; + const req = new TransferTableRequest( this.tableToTransfer.name, this.database, this.getSelectedSchemaForTable(this.tableToTransfer), primaryKeyNames); + this._crud.transferTable( req ).subscribe( + res => { + const result = res; + if (result.error) { + this._toast.exception(result, 'Could not transfer collection:'); + } else { + this._toast.success('Transfered collection ' + this.tableToTransfer.name, result.generatedQuery); + this.updateExistingSchemas(); + this.selectedSchemas.delete(this.tableToTransfer.name); + this._leftSidebar.setSchema(new SchemaRequest('/views/schema-editing/', true, 2, false), this._router); + } + this.getTables(); + }, err => { + this._toast.error('Could not transfer collection'); + console.log(err); + } + ).add(() => { + this.transferTableModal.hide(); + }); + } + + selectSchemaForTable(table : TableModel, selectedSchema : string) { + this.selectedSchemas.set(table.name, selectedSchema); + } + + getSelectedSchemaForTable(table : TableModel) { + return this.selectedSchemas.get(table.name); + } + + getAvailableSchemas (): Namespace[] { + if(!this.namespaces) { return []; } + return this.namespaces.filter( (n: Namespace) => { + return n.name != this.database; + }); + } + + private updateExistingSchemas() { + this._crud.getSchema(new SchemaRequest('views/querying/console/', false, 1, false)).subscribe( + res => { + this.namespaces = []; + for (const namespace of res) { + this.namespaces.push(namespace); + } + } + ); + } + + initTransferTableModal(table : TableModel ){ + let selectedSchema = this.getSelectedSchemaForTable(table) + if (selectedSchema == undefined) { + return; + } + this.tableToTransfer = table; + this.transferTableModal.show(); + } + + clearTransferTableModal(){ + this.selectedStore = null; + } + } class TableModel { diff --git a/src/app/views/schema-editing/edit-columns/edit-columns.component.html b/src/app/views/schema-editing/edit-columns/edit-columns.component.html index 02d9a0a1..0621118f 100644 --- a/src/app/views/schema-editing/edit-columns/edit-columns.component.html +++ b/src/app/views/schema-editing/edit-columns/edit-columns.component.html @@ -524,7 +524,35 @@
Data placements
+ +
+
+
+
Schema Evolutions
+
+
+
+ Merge columns +
+
+

Currently, only the merge of non-primary, varchar columns is permitted.

+
+ +
+
+
+ + + + +
+ +
+
+
diff --git a/src/app/views/schema-editing/edit-columns/edit-columns.component.ts b/src/app/views/schema-editing/edit-columns/edit-columns.component.ts index e0b68ef5..5f4377f7 100644 --- a/src/app/views/schema-editing/edit-columns/edit-columns.component.ts +++ b/src/app/views/schema-editing/edit-columns/edit-columns.component.ts @@ -6,7 +6,7 @@ import {CrudService} from '../../../services/crud.service'; import {DbColumn, FieldType, Index, ModifyPartitionRequest, PartitionFunctionModel, PartitioningRequest, PolyType, ResultSet, StatisticColumnSet, StatisticTableSet, TableConstraint} from '../../../components/data-view/models/result-set.model'; import {ToastDuration, ToastService} from '../../../components/toast/toast.service'; import {FormControl, FormGroup, Validators} from '@angular/forms'; -import {ColumnRequest, ConstraintRequest, EditTableRequest, MaterializedRequest} from '../../../models/ui-request.model'; +import {ColumnRequest, ConstraintRequest, EditTableRequest, MaterializedRequest, MergeColumnsRequest} from '../../../models/ui-request.model'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import {CatalogColumnPlacement, MaterializedInfos, Placements, PlacementType, Store} from '../../adapters/adapter.model'; import {ModalDirective} from 'ngx-bootstrap/modal'; @@ -39,6 +39,12 @@ export class EditColumnsComponent implements OnInit, OnDestroy { confirmConstraint = -1; newPrimaryKey: DbColumn[]; + //merge columns handling + mergedColumnName = ''; + joinString = ''; + mergeableColumns: DbColumn[]; + columnsToMerge: DbColumn[] = []; + uniqueConstraintName = ''; proposedConstraintName = 'constraintName'; @@ -171,6 +177,7 @@ export class EditColumnsComponent implements OnInit, OnDestroy { this.partitioningRequest.column = this.resultSet.header[0].name; // deep copy: from: https://stackoverflow.com/questions/35504310/deep-copy-an-array-in-angular-2-typescript this.newPrimaryKey = this.resultSet.header.map( x => Object.assign({}, x)); + this.mergeableColumns = this.resultSet.header.filter(x => x.dataType == 'VARCHAR' && !x.primary).map( x => Object.assign({}, x)); }, err => { this._toast.error('Could not load columns of the table.', null, ToastDuration.INFINITE); console.log(err); @@ -462,6 +469,50 @@ export class EditColumnsComponent implements OnInit, OnDestroy { ); } + mergeColumns() { + + if( this.columnsToMerge?.length < 2 ) { + this._toast.warn('Please select at least 2 columns to merge.', 'merged colum name'); + return; + } + if( this.mergedColumnName === '' ) { + this._toast.warn('Please provide a name for the merged column.', 'merged colum name'); + return; + } + if( ! this._crud.nameIsValid( this.mergedColumnName ) ){ + this._toast.warn(this._crud.invalidNameMessage('column'), 'invalid column name'); + return; + } + + if( this.resultSet.header + .filter( h => !this.columnsToMerge.map(h => h.name).includes(h.name)) + .filter( h => h.name === this.mergedColumnName ) + .length > 0 ) { + this._toast.warn( 'There already exists a column with this name. However, it is allowed to select one of the names of the columns to be merged', + 'invalid column name' ); + return; + } + + const req = new MergeColumnsRequest( this.tableId, this.columnsToMerge, this.mergedColumnName, this.joinString) + this._crud.mergeColumns( req ).subscribe( + res => { + const result = res; + if( result.error === undefined ){ + this.getColumns(); + this.getPlacementsAndPartitions(); + this.mergedColumnName = ''; + this.columnsToMerge = []; + this.joinString = ''; + } else { + this._toast.exception(result, null, 'server error', ToastDuration.INFINITE); + } + }, err => { + this._toast.error('An error occurred on the server.', null, ToastDuration.INFINITE); + console.log(err); + } + ); + } + addUniqueConstraint(){ if( this.uniqueConstraintName === '' ) { if (!this.proposedConstraintName) { diff --git a/src/app/views/schema-editing/edit-tables/edit-tables.component.html b/src/app/views/schema-editing/edit-tables/edit-tables.component.html index 2b23b471..fc24e8b6 100644 --- a/src/app/views/schema-editing/edit-tables/edit-tables.component.html +++ b/src/app/views/schema-editing/edit-tables/edit-tables.component.html @@ -11,6 +11,7 @@
Tables in {{schema}} [{{schemaType}}]
Table Truncate Drop + Transfer @@ -50,6 +51,26 @@
Tables in {{schema}} [{{schemaType}}]
+ +
+
+
+ + +
+ +
+
+ diff --git a/src/app/views/schema-editing/edit-tables/edit-tables.component.ts b/src/app/views/schema-editing/edit-tables/edit-tables.component.ts index 39cc57c4..ecfc43db 100644 --- a/src/app/views/schema-editing/edit-tables/edit-tables.component.ts +++ b/src/app/views/schema-editing/edit-tables/edit-tables.component.ts @@ -1,6 +1,6 @@ import {Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {CrudService} from '../../../services/crud.service'; -import {EditTableRequest, SchemaRequest} from '../../../models/ui-request.model'; +import {EditTableRequest, SchemaRequest, TransferTableRequest} from '../../../models/ui-request.model'; import {ActivatedRoute, Router} from '@angular/router'; import {DbColumn, Index, PolyType, ResultSet, Status} from '../../../components/data-view/models/result-set.model'; import {ToastDuration, ToastService} from '../../../components/toast/toast.service'; @@ -15,6 +15,11 @@ import {UtilService} from '../../../services/util.service'; import * as $ from 'jquery'; import {DbTable} from '../../uml/uml.model'; +class Namespace { + name: string; + id: string; +} + @Component({ selector: 'app-edit-tables', templateUrl: './edit-tables.component.html', @@ -22,6 +27,8 @@ import {DbTable} from '../../uml/uml.model'; }) export class EditTablesComponent implements OnInit, OnDestroy { + private readonly LOCAL_STORAGE_NAMESPACE_KEY = 'polypheny-namespace'; + types: PolyType[] = []; schema: string; schemaType: string; @@ -34,6 +41,10 @@ export class EditTablesComponent implements OnInit, OnDestroy { selectedStore; creatingTable = false; + activeNamespace: string; + namespaces: Namespace[]; + selectedSchemas = new Map(); // name of the table, name of the selected namespace + //export table showExportButton = false; exportProgress = 0.0; @@ -79,6 +90,7 @@ export class EditTablesComponent implements OnInit, OnDestroy { }); this.subscriptions.add(sub2); this.documentListener(); + this.updateExistingSchemas(); } ngOnDestroy() { @@ -105,6 +117,25 @@ export class EditTablesComponent implements OnInit, OnDestroy { }); } + getAvailableSchemas (): Namespace[] { + if(!this.namespaces) { return []; } + return this.namespaces.filter( (n: Namespace) => { + return n.name != this.schema; + }); + } + + private updateExistingSchemas() { + this._crud.getSchema(new SchemaRequest('views/querying/console/', false, 1, false)).subscribe( + res => { + this.namespaces = []; + for (const namespace of res) { + this.namespaces.push(namespace); + } + } + ); + } + + getTables() { this._crud.getTables(new EditTableRequest(this.schema)).subscribe( res => { @@ -368,6 +399,35 @@ export class EditTablesComponent implements OnInit, OnDestroy { } } + transferTable(table : TableModel) { + const req = new TransferTableRequest( table.name, this.schema, this.getSelectedSchemaForTable(table), null) + this._crud.transferTable( req ).subscribe( + res => { + const result = res; + if (result.error) { + this._toast.exception(result, 'Could not transfer table:'); + } else { + this._toast.success('Transfered table ' + table.name, result.generatedQuery); + this.updateExistingSchemas(); + this.selectedSchemas.delete(table.name); + this._leftSidebar.setSchema(new SchemaRequest('/views/schema-editing/', true, 2, false), this._router); + } + this.getTables(); + }, err => { + this._toast.error('Could not transfer table'); + console.log(err); + } + ); + } + + selectSchemaForTable(table : TableModel, selectedSchema : string) { + this.selectedSchemas.set(table.name, selectedSchema); + } + + getSelectedSchemaForTable(table : TableModel) { + return this.selectedSchemas.get(table.name); + } + initSocket() { const sub = this._crud.onSocketEvent().subscribe( msg => {