diff --git a/src/app/components/data-view/data-card/data-card.component.html b/src/app/components/data-view/data-card/data-card.component.html index b52cc33d..67fcae62 100644 --- a/src/app/components/data-view/data-card/data-card.component.html +++ b/src/app/components/data-view/data-card/data-card.component.html @@ -87,7 +87,7 @@ [animated]="true" [value]="100" color="primary"> - + @if (uploadProgress === -1) { +
+ + +
+ + } diff --git a/src/app/components/data-view/data-template/data-template.component.ts b/src/app/components/data-view/data-template/data-template.component.ts index f8e13a7c..53511ba1 100644 --- a/src/app/components/data-view/data-template/data-template.component.ts +++ b/src/app/components/data-view/data-template/data-template.component.ts @@ -93,6 +93,7 @@ export abstract class DataTemplateComponent implements OnInit, OnDestroy { this.webSocket = new WebSocket(); this._route.params.subscribe(route => { this.currentRoute.set(route['id']); + this.stopEditing(); }); this.entity = computed(() => { @@ -382,6 +383,10 @@ export abstract class DataTemplateComponent implements OnInit, OnDestroy { } } + stopEditing() { + this.editing = -1; + } + getBoolean(value: any): Boolean { switch (value) { case true: diff --git a/src/app/components/data-view/input/input.component.html b/src/app/components/data-view/input/input.component.html index 76a4d01d..06b43dee 100644 --- a/src/app/components/data-view/input/input.component.html +++ b/src/app/components/data-view/input/input.component.html @@ -3,33 +3,35 @@ - {{header.name}} + {{ header.name }} + [ngClass]="validate(inputElement)?.cssClass" (keyup)="onValueChange(inputElement.value, $event)" + (keydown)="restrictNumericInput($event)" + (paste)="sanitizePastedInput($event)"> - {{validate( inputElement )?.message}} + {{ validate( inputElement )?.message }} - {{header.name}} + {{ header.name }} - - - @if (header.nullable){ - + + + @if (header.nullable) { + } - {{header.name}} + {{ header.name }} - {{header.name}} + {{ header.name }} - {{validate( defaultInput )?.message}} + {{ validate( defaultInput )?.message }} diff --git a/src/app/components/data-view/input/input.component.ts b/src/app/components/data-view/input/input.component.ts index 9671c237..13c96eff 100644 --- a/src/app/components/data-view/input/input.component.ts +++ b/src/app/components/data-view/input/input.component.ts @@ -1,16 +1,4 @@ -import { - AfterViewInit, - Component, - ElementRef, - EventEmitter, - inject, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges, - ViewChild -} from '@angular/core'; +import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from '@angular/core'; import {FieldDefinition, UiColumnDefinition} from '../models/result-set.model'; import {DbmsTypesService} from '../../../services/dbms-types.service'; import * as $ from 'jquery'; @@ -244,4 +232,57 @@ export class InputComponent implements OnInit, OnChanges, AfterViewInit { this.valueChange.emit(file); } + // Somewhat hacky fix to restrict input to numbers without relying on type="number". + restrictNumericInput(event: KeyboardEvent) { + const allowedKeys = [ + 'Backspace', 'Tab', 'ArrowLeft', 'ArrowRight', 'Delete', 'Home', 'End' + ]; + + // Allow navigation and control keys + if (allowedKeys.includes(event.key)) { + return; + } + + // Allow digits, one dot, and one minus at the start + const current = (event.target as HTMLInputElement).value; + const key = event.key; + + // Prevent multiple dots + if (key === '.' && current.includes('.')) { + event.preventDefault(); + return; + } + + // Prevent minus except at the beginning + if (key === '-' && current.length > 0) { + event.preventDefault(); + return; + } + + // Prevent non-numeric characters + if (!/[0-9.-]/.test(key)) { + event.preventDefault(); + } + } + + // Enables pasting via context menu + sanitizePastedInput(event: ClipboardEvent) { + const pasted = event.clipboardData?.getData('text') ?? ''; + const sanitized = pasted.replace(/[^0-9.-]/g, ''); + + event.preventDefault(); + + const input = event.target as HTMLInputElement; + const {selectionStart, selectionEnd, value} = input; + + // Replace selected text with sanitized paste + input.value = + value.slice(0, selectionStart ?? 0) + + sanitized + + value.slice(selectionEnd ?? 0); + + // Trigger manual input event + const nativeInputEvent = new Event('input', {bubbles: true}); + input.dispatchEvent(nativeInputEvent); + } } diff --git a/src/app/components/toast-exposer/toast-exposer.component.ts b/src/app/components/toast-exposer/toast-exposer.component.ts index 34dab17c..f049f974 100644 --- a/src/app/components/toast-exposer/toast-exposer.component.ts +++ b/src/app/components/toast-exposer/toast-exposer.component.ts @@ -27,8 +27,10 @@ export class ToastExposerComponent implements OnInit { color: toast.type, autohide: true }; - const componentRef: ComponentRef = this.toaster.addToast(ToastComponent, {...options}); - componentRef.instance.toast.next(toast); + if (this.toaster) { + const componentRef: ComponentRef = this.toaster.addToast(ToastComponent, {...options}); + componentRef.instance.toast.next(toast); + } } }); } diff --git a/src/app/components/toast-exposer/toast/toast.component.html b/src/app/components/toast-exposer/toast/toast.component.html index 7a2377dc..8bb27dea 100644 --- a/src/app/components/toast-exposer/toast/toast.component.html +++ b/src/app/components/toast-exposer/toast/toast.component.html @@ -1,16 +1,24 @@
- - {{(toast | async)?.title}} - + + + {{ (toast | async)?.title }} + @if ((toast | async)?.generatedQuery) { + + @if (showCopied) { + Copied! + } @else { + + } + + }
- {{(toast | async)?.message}} + {{ (toast | async)?.message }}
@@ -42,10 +50,10 @@
Exception
- {{e.message}} + {{ e.message }} -

This is a dynamic toast no {{toast.toast?.index}} {{toast.toast?.clock}}

+

This is a dynamic toast no {{ toast.toast?.index }} {{ toast.toast?.clock }}

diff --git a/src/app/components/toast-exposer/toast/toast.component.scss b/src/app/components/toast-exposer/toast/toast.component.scss index e69de29b..679b14df 100644 --- a/src/app/components/toast-exposer/toast/toast.component.scss +++ b/src/app/components/toast-exposer/toast/toast.component.scss @@ -0,0 +1,12 @@ +.copy-query-btn { + cursor: pointer; +} + +.copy-query-btn .fa-code { + transition: transform 0.1s ease, color 0.1s ease; +} + +.copy-query-btn:hover .fa-code { + transform: scale(1.1); + color: black; +} \ No newline at end of file diff --git a/src/app/components/toast-exposer/toast/toast.component.ts b/src/app/components/toast-exposer/toast/toast.component.ts index 03e83690..6df9d3e7 100644 --- a/src/app/components/toast-exposer/toast/toast.component.ts +++ b/src/app/components/toast-exposer/toast/toast.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild} from '@angular/core'; +import {ChangeDetectorRef, Component, ElementRef, forwardRef, inject, Input, Renderer2, ViewChild} from '@angular/core'; import {Toast} from '../toaster.model'; import {KeyValue} from '@angular/common'; import {ModalDirective} from 'ngx-bootstrap/modal'; @@ -6,6 +6,7 @@ import {ResultException} from '../../data-view/models/result-set.model'; import {ToastComponent as ToastParent, ToasterService} from '@coreui/angular'; import {BehaviorSubject} from 'rxjs'; +import {UtilService} from '../../../services/util.service'; @Component({ selector: 'app-toast', @@ -23,6 +24,9 @@ export class ToastComponent extends ToastParent { public toast: BehaviorSubject = new BehaviorSubject(null); + public readonly _util = inject(UtilService); + showCopied = false; + constructor( public override hostElement: ElementRef, public override renderer: Renderer2, @@ -58,7 +62,9 @@ export class ToastComponent extends ToastParent { } copyGeneratedQuery(toast: Toast) { - //this._util.clipboard(toast.generatedQuery); + this._util.clipboard(toast.generatedQuery); + this.showCopied = true; + setTimeout(() => this.showCopied = false, 3500); } } diff --git a/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts b/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts index 618b7b49..48d03171 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/edit-notebook.component.ts @@ -456,13 +456,15 @@ export class EditNotebookComponent implements OnInit, OnChanges, OnDestroy { this._notebooks.restartKernel(this.session.kernel.id).pipe( tap(() => this.nb.setKernelStatusBusy()), delay(2500) // time for the kernel to restart - ).subscribe(() => { - this.nb.requestExecutionState(); - if (this.executeAllAfterRestart) { - this.nb.executeAll(); - } - }, () => { - this._toast.error('Unable to restart the kernel.'); + ).subscribe({ + next: () => { + this.nb.requestExecutionState(); + this._toast.success('Kernel has been restarted.'); + if (this.executeAllAfterRestart) { + this.nb.executeAll(); + } + }, + error: () => this._toast.error('Unable to restart the kernel.') }); this.restartKernelModal.hide(); } diff --git a/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.html b/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.html index e1482113..0b262b8f 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.html +++ b/src/app/plugins/notebooks/components/edit-notebook/nb-cell/nb-cell.component.html @@ -47,7 +47,7 @@ (mouseleave)="resetDeleteConfirm()" tooltip="Delete cell" container="body"> -
diff --git a/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts b/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts index 856d5f4c..644b44af 100644 --- a/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts +++ b/src/app/plugins/notebooks/components/edit-notebook/notebook-wrapper.ts @@ -1,28 +1,8 @@ import {KernelSpec, NotebookContent} from '../../models/notebooks-response.model'; -import { - CellDisplayDataOutput, - CellErrorOutput, - CellExecuteResultOutput, - CellOutputType, - CellStreamOutput, - Notebook, - NotebookCell -} from '../../models/notebook.model'; +import {CellDisplayDataOutput, CellErrorOutput, CellExecuteResultOutput, CellOutputType, CellStreamOutput, Notebook, NotebookCell} from '../../models/notebook.model'; import * as uuid from 'uuid'; import {NotebooksWebSocket} from '../../services/notebooks-webSocket'; -import { - KernelDisplayData, - KernelErrorMsg, - KernelExecuteInput, - KernelExecuteReply, - KernelExecuteResult, - KernelInterruptReply, - KernelMsg, - KernelShutdownReply, - KernelStatus, - KernelStream, - KernelUpdateDisplayData -} from '../../models/kernel-response.model'; +import {KernelDisplayData, KernelErrorMsg, KernelExecuteInput, KernelExecuteReply, KernelExecuteResult, KernelInterruptReply, KernelMsg, KernelShutdownReply, KernelStatus, KernelStream, KernelUpdateDisplayData} from '../../models/kernel-response.model'; import {interval, Subscription} from 'rxjs'; export class NotebookWrapper { @@ -58,7 +38,7 @@ export class NotebookWrapper { this.kernelStatus = 'unknown'; }); this.socket.requestExecutionState(); - this.keepAlive = interval(60000).subscribe(() => this.socket?.requestExecutionState()); // prevent 300s timeout + this.keepAlive = interval(10000).subscribe(() => this.socket?.requestExecutionState()); // prevent 300s timeout } closeSocket() { diff --git a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.html b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.html index a43e1fa9..2e30140d 100644 --- a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.html +++ b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.html @@ -2,10 +2,14 @@

Notebooks


-
+
- Jupyter Server + Jupyter Server + @if (connectedInstance) { + ({{ connectedInstance.host.alias }}) + } +
{{ serverStatus ? 'Online' : 'Offline' }}
@@ -16,21 +20,23 @@
{{ serverStatus ? 'Online' : 'Offline' }}
- - - +
+ + + +
diff --git a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts index 3272f464..1cf208a8 100644 --- a/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts +++ b/src/app/plugins/notebooks/components/notebooks-dashboard/notebooks-dashboard.component.ts @@ -38,6 +38,7 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { pluginLoaded = false; sessionSubscription = null; instances: DockerInstanceInfo[] = []; + connectedInstance: DockerInstanceInfo | null = null; constructor() { } @@ -53,6 +54,7 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { } }); this.getPluginStatus(); + this.updateDockerInstanceInfo(); const sub = interval(10000).subscribe(() => { this.getServerStatus(); @@ -100,7 +102,7 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { }); const paths = this.sessions.map(s => this._notebooks.getPathFromSession(s)); this.notebookPaths = paths.map(p => p.replace('notebooks/', '') - .replace(/\//g, '/\u200B')); // zero-width space => allow soft line breaks after '/' + .replace(/\//g, '/\u200B')); // zero-width space => allow soft line breaks after '/' this.isPreferredSession = this.sessions.map((s, i) => this._content.getPreferredSessionId(paths[i]) === s.id ); @@ -128,7 +130,10 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { this.creating = false; console.log(err); } - }).add(() => this.getServerStatus()); + }).add(() => { + this.getServerStatus(); + this.updateDockerInstanceInfo(); + }); } destroyContainer() { @@ -143,15 +148,19 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { this.serverRunning.emit(false); this.restartContainerModal.hide(); this._notebooks.restartContainer().subscribe({ - next: res => { + next: () => { this._toast.success('Successfully restarted the container.'); this._content.updateSessions(); this._content.update(); }, error: err => { + console.log('server restart error', err); this._toast.error('An error occurred while restarting the container!'); } - }).add(() => this.getServerStatus()); + }).add(() => { + this.getServerStatus(); + this.updateDockerInstanceInfo(); + }); } getPluginStatus() { @@ -185,4 +194,10 @@ export class NotebooksDashboardComponent implements OnInit, OnDestroy { }); } + private updateDockerInstanceInfo() { + this._notebooks.getDockerInstance().subscribe({ + next: res => this.connectedInstance = res + }); + } + } diff --git a/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts b/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts index 8830ff7d..ad1c5769 100644 --- a/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts +++ b/src/app/plugins/notebooks/services/notebooks-sidebar.service.ts @@ -156,7 +156,10 @@ export class NotebooksSidebarService { this.subscriptions = new Subscription(); this.subscriptions.add(this._content.onContentChange().subscribe(() => this.update())); this.subscriptions.add(this._content.onSessionsChange().subscribe(() => this.updateSidebar())); - this._leftSidebar.open(); + if (!this._leftSidebar.isVisible()) { + this._leftSidebar.open(); + this.update(); + } } close() { diff --git a/src/app/plugins/notebooks/services/notebooks.service.ts b/src/app/plugins/notebooks/services/notebooks.service.ts index 4a20462c..35a0cbc8 100644 --- a/src/app/plugins/notebooks/services/notebooks.service.ts +++ b/src/app/plugins/notebooks/services/notebooks.service.ts @@ -1,14 +1,7 @@ import {Injectable} from '@angular/core'; import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import {WebuiSettingsService} from '../../../services/webui-settings.service'; -import { - Content, - KernelResponse, - KernelSpecs, - NotebookContent, - SessionResponse, - StatusResponse -} from '../models/notebooks-response.model'; +import {Content, KernelResponse, KernelSpecs, NotebookContent, SessionResponse, StatusResponse} from '../models/notebooks-response.model'; import {Notebook} from '../models/notebook.model'; import * as uuid from 'uuid'; import {forkJoin} from 'rxjs'; @@ -190,13 +183,17 @@ export class NotebooksService { } restartContainer() { - return this._http.post(`${this.httpUrl}/container/restart`, '', this.httpOptions); + return this._http.post(`${this.httpUrl}/container/restart`, null, {responseType: 'text'}); } getDockerInstances() { return this._http.get(`${this.httpUrl}/container/getDockerInstances`); } + getDockerInstance() { + return this._http.get(`${this.httpUrl}/container/getDockerInstance`); + } + createContainer(id: number) { return this._http.post(`${this.httpUrl}/container/create?dockerInstance=${id}`, '', this.httpOptions); } diff --git a/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts index bf37fe66..574ef8e8 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts +++ b/src/app/plugins/workflows/components/workflow-viewer/editor/workflow-editor.ts @@ -137,9 +137,13 @@ export class WorkflowEditor { return; // https://github.com/retejs/rete/issues/204 } else if (context.type === 'nodetranslated') { this.translateSubjects[this.nodeIdToActivityId.get(context.data.id)].next(context.data.position); - }/* else if (!['pointermove', 'render', 'rendered', 'rendered', 'zoom', 'zoomed', 'translate', 'translated', 'nodetranslate', 'unmount'].includes(context.type)) { + } else if (context.type === 'pointerdown' && context.data.event.button === 0) { + document.body.style.userSelect = 'none'; // ensure dragging out of the area does not select activity notes + } else if (context.type === 'pointerup' && context.data.event.button === 0) { + document.body.style.userSelect = ''; // restore userSelect + } /* else if (!['pointermove', 'render', 'rendered', 'rendered', 'zoom', 'zoomed', 'translate', 'translated', 'nodetranslate', 'unmount'].includes(context.type)) { console.log(context); - }*/ + } */ return context; }); diff --git a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html index 78381837..389b12f0 100644 --- a/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html +++ b/src/app/plugins/workflows/components/workflow-viewer/left-menu/left-menu.component.html @@ -1,4 +1,4 @@ -
@if (visible()) {
@@ -61,7 +61,7 @@

Add Activity

-