/** * TEMS Assets Management Component * * Provides CRUD operations for EDC assets, integrated with Hubl. * Uses sib-core's DataspaceConnectorStore for all operations. * * @customElement solid-tems-assets-management */ import * as utils from "@helpers"; import { localized, msg } from "@lit/localize"; import { OrbitDSPComponent } from "@startinblox/solid-tems-shared"; import { css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; interface Asset { "@id": string; "@type"?: string; properties?: Record; dataAddress?: { "@type"?: string; type?: string; baseUrl?: string; endpoint?: string; }; createdAt?: number; } interface AssetInput { "@id": string; properties?: Record; dataAddress?: { "@type": string; type: string; baseUrl?: string; }; } @customElement("solid-tems-assets-management") @localized() export class TemsAssetsManagement extends OrbitDSPComponent { constructor() { super(); utils.setupCacheInvalidation(this, { keywords: ["assets", "management"], }); } static styles = css` :host { display: block; } .management-container { padding: 1rem; } .management-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; } .management-header h2 { margin: 0; font-size: 1.5rem; color: #333; } .toolbar { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 1rem; } .toolbar tems-search-bar { flex: 1; min-width: 200px; } .toolbar-actions { display: flex; gap: 0.5rem; align-items: center; } .sort-select { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem; } .btn-card { padding: 0.25rem 0.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.75rem; transition: background-color 0.2s; } .btn-card-secondary { background-color: #f5f5f5; color: #333; border: 1px solid #ddd; } .btn-card-secondary:hover { background-color: #e0e0e0; } .btn-card-danger { background-color: #f44336; color: white; } .btn-card-danger:hover { background-color: #d32f2f; } .assets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } .asset-card { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); cursor: pointer; transition: box-shadow 0.2s, border-color 0.2s; } .asset-card:hover { border-color: #e91e63; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .asset-id { font-size: 0.75rem; color: #666; margin-bottom: 0.5rem; word-break: break-all; } .asset-title { font-size: 1.125rem; font-weight: 600; color: #333; margin-bottom: 0.5rem; } .asset-description { font-size: 0.875rem; color: #666; margin-bottom: 1rem; line-height: 1.4; } .asset-properties { font-size: 0.75rem; color: #888; margin-bottom: 1rem; padding: 0.5rem; background: #f9f9f9; border-radius: 4px; } .asset-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } .empty-state { text-align: center; padding: 3rem; color: #666; } .empty-icon { font-size: 4rem; margin-bottom: 1rem; } .loading { text-align: center; padding: 2rem; color: #666; } .message { padding: 1rem; border-radius: 4px; margin-bottom: 1rem; } .message-error { background-color: #ffebee; color: #c62828; border: 1px solid #ef9a9a; } .message-success { background-color: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; } .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; border-radius: 8px; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid #e0e0e0; } .modal-header h3 { margin: 0; } .modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #666; } .modal-body { padding: 1.5rem; } .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } .form-group { display: flex; flex-direction: column; gap: 0.25rem; } .form-group.full-width { grid-column: 1 / -1; } .form-label { font-size: 0.875rem; font-weight: 500; color: #333; } .form-input, .form-textarea { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem; } .form-textarea { min-height: 80px; resize: vertical; } .form-actions { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; } .form-actions tems-button { min-width: 100px; } .validation-popup .validation-message { background: #fff3e0; padding: 1rem; border-radius: 4px; border: 1px solid #ffcc80; } .validation-details { margin-top: 1rem; font-size: 0.875rem; } .negotiation-item { display: flex; justify-content: space-between; padding: 0.5rem; background: #f5f5f5; border-radius: 4px; margin-top: 0.5rem; } .detail-section { display: flex; flex-direction: column; gap: 1rem; } .detail-row { display: flex; flex-direction: column; gap: 0.25rem; } .detail-row strong { color: #666; font-size: 0.875rem; } .detail-description { margin: 0; line-height: 1.6; color: #333; } .rating-badge { display: inline-block; padding: 0.25rem 0.5rem; background: #ffebee; color: #c62828; border-radius: 4px; font-size: 0.875rem; font-weight: 500; } .data-address-details { background: #f5f5f5; padding: 0.5rem; border-radius: 4px; font-size: 0.875rem; } .data-address-details a { color: #e91e63; word-break: break-all; } `; @property({ attribute: "header", type: String }) header = "Assets Management"; @state() private assets: Asset[] = []; @state() private isLoading = false; @state() private showCreateForm = false; @state() private editingAsset: Asset | null = null; @state() private viewingAsset: Asset | null = null; @state() private searchTerm = ""; @state() private sortBy: "id" | "title" | "created" = "id"; @state() private sortDirection: "asc" | "desc" = "asc"; @state() private errorMessage = ""; @state() private successMessage = ""; @state() private validationError: { assetId: string; validationType: "update" | "delete"; reason: "agreements" | "negotiations"; message: string; details?: any; } | null = null; override async _afterAttach() { await super._afterAttach(); // Load assets after store is initialized if (this.storeService) { this.loadAssets(); } } private async loadAssets() { if (!this.storeService) { this.showError("Store not initialized. Check connector configuration."); return; } this.isLoading = true; this.clearMessages(); try { const assets = await this.storeService.getAllAssets(); this.assets = Array.isArray(assets) ? assets : []; } catch (error: any) { console.error("Failed to load assets:", error); this.showError(`Failed to load assets: ${error.message}`); } finally { this.isLoading = false; } } private async createAsset(assetData: AssetInput) { if (!this.storeService) return false; try { await this.storeService.createAsset(assetData); this.showSuccess("Asset created successfully!"); this.showCreateForm = false; await this.loadAssets(); this.dispatchEvent( new CustomEvent("asset-created", { detail: { asset: assetData }, bubbles: true, composed: true, }), ); return true; } catch (error: any) { this.showError(`Failed to create asset: ${error.message}`); return false; } } private async updateAsset(assetId: string, assetData: AssetInput) { if (!this.storeService) return false; try { await this.storeService.updateAsset(assetId, assetData); this.showSuccess("Asset updated successfully!"); this.editingAsset = null; await this.loadAssets(); this.dispatchEvent( new CustomEvent("asset-updated", { detail: { assetId, asset: assetData }, bubbles: true, composed: true, }), ); return true; } catch (error: any) { if (error.name === "AssetValidationError") { this.validationError = { assetId: error.assetId, validationType: error.validationType, reason: error.reason, message: error.message, details: error.details, }; } else { this.showError(`Failed to update asset: ${error.message}`); } return false; } } private async deleteAsset(assetId: string) { if (!this.storeService) return; if (!confirm(`Are you sure you want to delete asset '${assetId}'?`)) { return; } try { await this.storeService.deleteAsset(assetId); this.showSuccess("Asset deleted successfully!"); await this.loadAssets(); this.dispatchEvent( new CustomEvent("asset-deleted", { detail: { assetId }, bubbles: true, composed: true, }), ); } catch (error: any) { if (error.name === "AssetValidationError") { this.validationError = { assetId: error.assetId, validationType: error.validationType, reason: error.reason, message: error.message, details: error.details, }; } else { this.showError(`Failed to delete asset: ${error.message}`); } } } private handleCreateAsset = async (e: Event) => { e.preventDefault(); const form = e.target as HTMLFormElement; const formData = new FormData(form); const properties: Record = { name: formData.get("name") as string, }; const description = formData.get("description") as string; if (description) properties.description = description; const author = formData.get("author") as string; if (author) properties.author = author; const contenttype = formData.get("contenttype") as string; if (contenttype) properties.contenttype = contenttype; const assetData: AssetInput = { "@id": formData.get("assetId") as string, properties, }; const dataAddressType = formData.get("dataAddressType") as string; const baseUrl = formData.get("baseUrl") as string; if (dataAddressType) { assetData.dataAddress = { "@type": "DataAddress", type: dataAddressType, }; if (baseUrl) { assetData.dataAddress.baseUrl = baseUrl; } } await this.createAsset(assetData); }; private handleEditAsset = async (e: Event) => { e.preventDefault(); if (!this.editingAsset) return; const form = e.target as HTMLFormElement; const formData = new FormData(form); const properties: Record = { name: formData.get("name") as string, }; const description = formData.get("description") as string; if (description) properties.description = description; const author = formData.get("author") as string; if (author) properties.author = author; const contenttype = formData.get("contenttype") as string; if (contenttype) properties.contenttype = contenttype; const assetData: AssetInput = { "@id": this.editingAsset["@id"], properties, }; const dataAddressType = formData.get("dataAddressType") as string; const baseUrl = formData.get("baseUrl") as string; if (dataAddressType) { assetData.dataAddress = { "@type": "DataAddress", type: dataAddressType, }; if (baseUrl) { assetData.dataAddress.baseUrl = baseUrl; } } await this.updateAsset(this.editingAsset["@id"], assetData); }; private showError(message: string) { this.errorMessage = message; this.successMessage = ""; setTimeout(() => { this.errorMessage = ""; }, 5000); } private showSuccess(message: string) { this.successMessage = message; this.errorMessage = ""; setTimeout(() => { this.successMessage = ""; }, 3000); } private clearMessages() { this.errorMessage = ""; this.successMessage = ""; } private get filteredAssets() { let filtered = [...this.assets]; if (this.searchTerm) { const term = this.searchTerm.toLowerCase(); filtered = filtered.filter((asset) => { const id = asset["@id"]?.toLowerCase() || ""; const title = asset.properties?.["dcterms:title"]?.toLowerCase() || ""; const description = asset.properties?.["dcterms:description"]?.toLowerCase() || ""; return ( id.includes(term) || title.includes(term) || description.includes(term) ); }); } filtered.sort((a, b) => { let aVal: string | number; let bVal: string | number; switch (this.sortBy) { case "id": aVal = a["@id"] || ""; bVal = b["@id"] || ""; break; case "title": aVal = a.properties?.["dcterms:title"] || a["@id"] || ""; bVal = b.properties?.["dcterms:title"] || b["@id"] || ""; break; case "created": aVal = a.createdAt || 0; bVal = b.createdAt || 0; break; default: aVal = a["@id"] || ""; bVal = b["@id"] || ""; } if (typeof aVal === "string" && typeof bVal === "string") { aVal = aVal.toLowerCase(); bVal = bVal.toLowerCase(); } let result = 0; if (aVal < bVal) result = -1; else if (aVal > bVal) result = 1; return this.sortDirection === "desc" ? -result : result; }); return filtered; } private _handleSearch(e: CustomEvent) { this.searchTerm = e.detail || ""; } render() { return html`
`} @clicked=${() => { this.showCreateForm = true; }} >
${this.renderMessages()}
{ this.sortDirection = this.sortDirection === "asc" ? "desc" : "asc"; }} > `} ?disabled=${this.isLoading} @clicked=${() => this.loadAssets()} >
${this.showCreateForm ? this.renderCreateForm() : nothing} ${this.editingAsset ? this.renderEditForm() : nothing} ${this.viewingAsset ? this.renderDetailView() : nothing} ${this.validationError ? this.renderValidationPopup() : nothing} ${ this.isLoading ? html`
Loading assets...
` : this.renderAssetsList() }
`; } private renderMessages() { return html` ${ this.errorMessage ? html`
${this.errorMessage}
` : nothing } ${ this.successMessage ? html`
${this.successMessage}
` : nothing } `; } private renderAssetsList() { const assets = this.filteredAssets; if (assets.length === 0) { return html`
📦

No assets found

Create your first asset to get started.

`; } return html`
${repeat( assets, (asset) => asset["@id"], (asset) => this.renderAssetCard(asset), )}
`; } private getAssetTitle(asset: Asset): string { return ( asset.properties?.["name"] || asset.properties?.["dcterms:title"] || asset["@id"] ); } private getAssetDescription(asset: Asset): string { return ( asset.properties?.["description"] || asset.properties?.["dcterms:description"] || "" ); } private renderAssetCard(asset: Asset) { const title = this.getAssetTitle(asset); const description = this.getAssetDescription(asset); const truncatedDescription = description.length > 150 ? `${description.substring(0, 150)}...` : description; return html`
{ this.viewingAsset = asset; }}>
ID: ${asset["@id"]}
${title}
${truncatedDescription || msg("No description")}
${ asset.dataAddress ? html`
${msg("Data Address")}:
${msg("Type")}: ${asset.dataAddress.type || "N/A"}
${ asset.dataAddress.baseUrl || asset.dataAddress.endpoint ? html`
URL: ${asset.dataAddress.baseUrl || asset.dataAddress.endpoint}
` : nothing }
` : nothing }
e.stopPropagation()}>
`; } private renderCreateForm() { return html` `; } private renderEditForm() { if (!this.editingAsset) return nothing; const asset = this.editingAsset; const name = this.getAssetTitle(asset); const description = this.getAssetDescription(asset); const author = asset.properties?.["author"] || ""; const contenttype = asset.properties?.["contenttype"] || ""; return html` `; } private renderDetailView() { if (!this.viewingAsset) return nothing; const asset = this.viewingAsset; const title = this.getAssetTitle(asset); const description = this.getAssetDescription(asset); const author = asset.properties?.["author"] || ""; const rating = asset.properties?.["rating"] || ""; const contentType = asset.properties?.["contenttype"] || ""; return html` `; } private renderValidationPopup() { if (!this.validationError) return nothing; const { assetId, validationType, reason, message, details } = this.validationError; const reasonTitle = reason === "agreements" ? "Active Contract Agreements" : "Active Negotiations"; const actionType = validationType === "update" ? "update" : "delete"; return html` `; } }