From b60fc3c1d6ddd3aa07e9cf3649c0d75e1576e158 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 29 Sep 2026 09:09:47 +0200 Subject: [PATCH] Show each kind of store as a resource of its own The resource palette now lists an S3 storage and a PostgreSQL database as two cards, each with the label and icon its type declares, and the node on the canvas takes the same. A step's resource field names a kind, so a storage operation links to either store. Co-Authored-By: Claude Opus 5.5 --- src/app/models/flow.ts | 20 +++++--- .../services/resources/resources-call.fake.ts | 46 +++++++++++++---- .../services/resources/resources-call.spec.ts | 31 +++++++----- src/app/services/resources/resources-call.ts | 3 ++ .../nodes/resource-node/resource-node.html | 2 +- .../nodes/resource-node/resource-node.ts | 8 ++- .../shared/resources-list/resources-list.html | 10 ++-- .../resources-list/resources-list.spec.ts | 50 ++++++++++++------- .../shared/resources-list/resources-list.ts | 6 ++- src/app/utilities/rete-editor.spec.ts | 12 ++--- src/app/utilities/rete-editor.ts | 10 ++-- 11 files changed, 131 insertions(+), 67 deletions(-) diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index 91ef700..ad538f7 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -241,9 +241,15 @@ export type FlowResource = { laneId?: string | null; }; -/** What the editor is told about a kind of resource, as BlockType says about a kind of block. */ +/** What the editor is told about a type of resource, as BlockType says about a type of block. */ export type ResourceType = { type: string; + /** What a step links to: an S3 storage and a PostgreSQL database are both of the Storage kind. */ + kind: string; + /** How the editor names it. */ + label: string; + /** A Bootstrap icon class, or null for the default one. */ + icon?: string | null; description: string; exampleEndpoint?: string | null; configurationType: string | null; @@ -322,20 +328,20 @@ export const FLOW_RESOURCE_PORT_KEY = '__resource'; export const FLOW_RESOURCE_LINK_PORT_KEY = '__resourceRef'; export const FLOW_RESOURCE_SOCKET_TYPE = '__FLOW_RESOURCE__'; -/** The schema flag that says a field holds the id of a resource, and of which type. */ +/** The schema flag that says a field holds the id of a resource, and of which kind. */ export const RESOURCE_REF_SCHEMA_KEY = 'x-resource-ref'; /** - * The field a block links a resource through, from its schema: its name and the type of resource + * The field a block links a resource through, from its schema: its name and the kind of resource * it takes. A block has at most one today; the first is the one the canvas draws. */ -export function resourceRefField(schema: BlockTypeSchema | undefined | null): { field: string; resourceType: string } | null { +export function resourceRefField(schema: BlockTypeSchema | undefined | null): { field: string; resourceKind: string } | null { const properties = (schema as Record | null | undefined)?.['properties']; if (!properties || typeof properties !== 'object') return null; for (const [field, raw] of Object.entries(properties as Record)) { - const resourceType = (raw as Record | null)?.[RESOURCE_REF_SCHEMA_KEY]; - if (typeof resourceType === 'string' && resourceType) { - return { field, resourceType }; + const resourceKind = (raw as Record | null)?.[RESOURCE_REF_SCHEMA_KEY]; + if (typeof resourceKind === 'string' && resourceKind) { + return { field, resourceKind }; } } return null; diff --git a/src/app/services/resources/resources-call.fake.ts b/src/app/services/resources/resources-call.fake.ts index 51cce1b..34c9847 100644 --- a/src/app/services/resources/resources-call.fake.ts +++ b/src/app/services/resources/resources-call.fake.ts @@ -6,32 +6,56 @@ import { FlowResource, ResourceType } from '@models/flow'; import { Observable, of } from 'rxjs'; import { ResourcesCallServiceBase } from './resource-call.base'; -export const FAKE_STORAGE_RESOURCE_TYPE: ResourceType = { - type: 'Storage', - description: 'A storage the flow keeps data in. It is not a step.', - exampleEndpoint: '/resources/types/Storage/example', - configurationType: 'StorageResourceConfiguration', - configurationClass: 'it.cnr.isti.workflow.manager.storage.StorageResourceConfiguration', +export const FAKE_S3_STORAGE_RESOURCE_TYPE: ResourceType = { + type: 'S3Storage', + kind: 'Storage', + label: 'S3 storage', + icon: 'bi-bucket', + description: 'An S3-compatible object store the flow keeps files in. It is not a step.', + exampleEndpoint: '/resources/types/S3Storage/example', + configurationType: 'S3StorageResourceConfiguration', + configurationClass: 'it.cnr.isti.workflow.manager.storage.s3.S3StorageResourceConfiguration', schema: { type: 'object', properties: { name: { type: 'string', 'x-ui-order': 10 }, - storageType: { type: 'string', 'x-ui-order': 20, 'x-ui-label': 'Storage type' }, - source: { type: 'string', enum: ['CATALOG', 'PERSONAL'], default: 'CATALOG', 'x-ui-order': 30 } + source: { type: 'string', enum: ['CATALOG', 'PERSONAL'], default: 'CATALOG', 'x-ui-order': 15 }, + scope: { type: 'string', enum: ['SHARED', 'PER_EXECUTION'], default: 'SHARED', 'x-ui-order': 30 } + } + } +}; + +export const FAKE_POSTGRES_STORAGE_RESOURCE_TYPE: ResourceType = { + type: 'PostgresStorage', + kind: 'Storage', + label: 'PostgreSQL database', + icon: 'bi-database', + description: 'A PostgreSQL database the flow keeps rows in. It is not a step.', + exampleEndpoint: '/resources/types/PostgresStorage/example', + configurationType: 'PostgresStorageResourceConfiguration', + configurationClass: 'it.cnr.isti.workflow.manager.storage.postgres.PostgresStorageResourceConfiguration', + schema: { + type: 'object', + properties: { + name: { type: 'string', 'x-ui-order': 10 }, + source: { type: 'string', enum: ['CATALOG', 'PERSONAL'], default: 'CATALOG', 'x-ui-order': 15 }, + initScript: { type: 'string', 'x-ui-order': 50 } } } }; export class ResourcesCallServiceFake extends ResourcesCallServiceBase { override retrieveAllResourceTypes(): Observable { - return of([FAKE_STORAGE_RESOURCE_TYPE]); + return of([FAKE_S3_STORAGE_RESOURCE_TYPE, FAKE_POSTGRES_STORAGE_RESOURCE_TYPE]); } override createEmptyResource(resourceType: string): Observable { + const type = [FAKE_S3_STORAGE_RESOURCE_TYPE, FAKE_POSTGRES_STORAGE_RESOURCE_TYPE] + .find((candidate) => candidate.type === resourceType); return of({ id: crypto.randomUUID(), - name: resourceType, - specificConfiguration: { type: 'StorageResourceConfiguration', name: resourceType } as FlowResource['specificConfiguration'], + name: type?.label ?? resourceType, + specificConfiguration: { type: type?.configurationType ?? resourceType, name: type?.label ?? resourceType } as FlowResource['specificConfiguration'], typeName: resourceType, nodeFamily: 'resource' as const }); diff --git a/src/app/services/resources/resources-call.spec.ts b/src/app/services/resources/resources-call.spec.ts index 8cb2b57..bd99a36 100644 --- a/src/app/services/resources/resources-call.spec.ts +++ b/src/app/services/resources/resources-call.spec.ts @@ -29,31 +29,36 @@ describe('ResourcesCallService', () => { http.expectOne(`${environment.apiUrl}/resources/types/configurations/catalog`).flush({ sharedDefinitions: { Shape: { type: 'string' } }, descriptors: [{ - type: 'Storage', - description: 'A storage the flow keeps data in.', - exampleEndpoint: '/resources/types/Storage/example', - configurationType: 'StorageResourceConfiguration', - configurationClass: 'it.cnr.isti.StorageResourceConfiguration', + type: 'S3Storage', + kind: 'Storage', + label: 'S3 storage', + icon: 'bi-bucket', + description: 'An object store the flow keeps files in.', + exampleEndpoint: '/resources/types/S3Storage/example', + configurationType: 'S3StorageResourceConfiguration', + configurationClass: 'it.cnr.isti.S3StorageResourceConfiguration', schema: { type: 'object', properties: { name: { type: 'string' } } } }] }); const [storage] = await types; - expect(storage.type).toBe('Storage'); - expect(storage.configurationType).toBe('StorageResourceConfiguration'); + expect(storage).toEqual(expect.objectContaining({ + type: 'S3Storage', kind: 'Storage', label: 'S3 storage', icon: 'bi-bucket', + configurationType: 'S3StorageResourceConfiguration' + })); expect((storage.schema as any)['sharedDefinitions']).toEqual({ Shape: { type: 'string' } }); }); it('asks the server for an empty resource, and marks it as one', async () => { - const resource = firstValueFrom(service.createEmptyResource('Storage')); + const resource = firstValueFrom(service.createEmptyResource('S3Storage')); - http.expectOne(`${environment.apiUrl}/resources/types/Storage/example`).flush({ + http.expectOne(`${environment.apiUrl}/resources/types/S3Storage/example`).flush({ id: 'r1', - name: 'Storage', - typeName: 'Storage', - specificConfiguration: { type: 'StorageResourceConfiguration', name: 'Storage' } + name: 'S3 storage', + typeName: 'S3Storage', + specificConfiguration: { type: 'S3StorageResourceConfiguration', name: 'S3 storage' } }); - expect(await resource).toEqual(expect.objectContaining({ id: 'r1', typeName: 'Storage', nodeFamily: 'resource' })); + expect(await resource).toEqual(expect.objectContaining({ id: 'r1', typeName: 'S3Storage', nodeFamily: 'resource' })); }); }); diff --git a/src/app/services/resources/resources-call.ts b/src/app/services/resources/resources-call.ts index 1999e78..f50d38f 100644 --- a/src/app/services/resources/resources-call.ts +++ b/src/app/services/resources/resources-call.ts @@ -39,6 +39,9 @@ export class ResourcesCallService extends ResourcesCallServiceBase { const value = toRecord(raw); return { type: String(value['type'] ?? ''), + kind: String(value['kind'] ?? value['type'] ?? ''), + label: String(value['label'] ?? value['type'] ?? ''), + icon: toNullableString(value['icon']), description: String(value['description'] ?? ''), exampleEndpoint: toApiPath(value['exampleEndpoint']), configurationType: toNullableString(value['configurationType']), diff --git a/src/app/shared/nodes/resource-node/resource-node.html b/src/app/shared/nodes/resource-node/resource-node.html index eb74899..9f379d8 100644 --- a/src/app/shared/nodes/resource-node/resource-node.html +++ b/src/app/shared/nodes/resource-node/resource-node.html @@ -26,7 +26,7 @@ }
- +
{{ resourceTitle() }} diff --git a/src/app/shared/nodes/resource-node/resource-node.ts b/src/app/shared/nodes/resource-node/resource-node.ts index dd2882f..f344349 100644 --- a/src/app/shared/nodes/resource-node/resource-node.ts +++ b/src/app/shared/nodes/resource-node/resource-node.ts @@ -57,6 +57,12 @@ export class ResourceNodeComponent extends GenericNodeComponent { /** The type of resource, as the palette names it: what a block's title would say. */ resourceTitle(): string { - return String(this.data?.data?.typeName ?? 'Resource'); + const typeName = String(this.data?.data?.typeName ?? ''); + return this.resourcesService.peekResourceType(typeName)?.label ?? (typeName || 'Resource'); + } + + /** Its type's own icon, so an object store and a database look different at a glance. */ + resourceIcon(): string { + return this.resourcesService.peekResourceType(String(this.data?.data?.typeName ?? ''))?.icon ?? 'bi-hdd-stack'; } } diff --git a/src/app/shared/resources-list/resources-list.html b/src/app/shared/resources-list/resources-list.html index 3678bc0..6ccc81d 100644 --- a/src/app/shared/resources-list/resources-list.html +++ b/src/app/shared/resources-list/resources-list.html @@ -28,16 +28,16 @@
- +
-
{{ resource.type }}
-
Resource
+
{{ resource.label }}
+
{{ resource.kind }} resource