/** * DSP Contract Catalog Component * * Displays user's contract negotiation history stored in localStorage. * Shows contracts with their current state, provider info, and access options. * * Usage: * ```html * * ``` * * Features: * - View all contracts with status * - Filter by state (active, pending, failed) * - Search by asset name or provider * - Access resources via EDR tokens * - Export/import contracts * * NOTE: This is a temporary client-side solution. * For production, use djangoldp-dsp backend. */ import { msg } from "@lit/localize"; import { type ContractState, DSPContractStorage, type StoredContract, } from "@startinblox/solid-tems-shared"; import { css, html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; @customElement("dsp-contract-catalog") export class DSPContractCatalog extends LitElement { @state() contracts: StoredContract[] = []; @state() filteredContracts: StoredContract[] = []; @state() filterState: ContractState | "ALL" = "ALL"; @state() searchQuery = ""; @state() selectedContract?: StoredContract; static styles = css` :host { display: block; font-family: system-ui, -apple-system, sans-serif; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding: 1rem; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .header h2 { margin: 0; color: #1976d2; } .stats { display: flex; gap: 1rem; font-size: 0.9em; } .stat { padding: 0.5rem 1rem; background: #f5f5f5; border-radius: 4px; } .stat strong { color: #1976d2; } .controls { display: flex; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: white; border-radius: 8px; } .filter-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; } .filter-btn { padding: 0.5rem 1rem; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 0.9em; transition: all 0.2s; } .filter-btn:hover { background: #f5f5f5; } .filter-btn.active { background: #1976d2; color: white; border-color: #1976d2; } .search-box { flex: 1; min-width: 200px; } .search-box input { width: 100%; padding: 0.5rem 1rem; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9em; } .contracts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1rem; } .contract-card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } .contract-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); } .contract-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; } .asset-name { font-weight: bold; font-size: 1.1em; color: #333; margin: 0 0 0.25rem 0; } .asset-id { font-size: 0.85em; color: #666; font-family: monospace; } .state-badge { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75em; font-weight: bold; text-transform: uppercase; } .state-REQUESTED { background: #fff3cd; color: #856404; } .state-OFFERED { background: #cfe2ff; color: #084298; } .state-AGREED { background: #d1e7dd; color: #0f5132; } .state-VERIFIED { background: #d1e7dd; color: #0f5132; } .state-FINALIZED { background: #d1e7dd; color: #0f5132; } .state-TERMINATED { background: #f8d7da; color: #842029; } .state-FAILED { background: #f8d7da; color: #842029; } .provider-info { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; padding: 0.75rem; background: #f8f9fa; border-radius: 4px; border-left: 4px solid var(--provider-color, #1976d2); } .provider-name { font-weight: 500; color: #333; } .contract-meta { font-size: 0.85em; color: #666; margin-bottom: 1rem; } .contract-meta div { margin-bottom: 0.25rem; } .contract-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } .action-btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; transition: all 0.2s; } .action-btn-primary { background: #1976d2; color: white; } .action-btn-primary:hover { background: #1565c0; } .action-btn-secondary { background: #6c757d; color: white; } .action-btn-secondary:hover { background: #5a6268; } .action-btn-danger { background: #dc3545; color: white; } .action-btn-danger:hover { background: #c82333; } .action-btn:disabled { opacity: 0.5; cursor: not-allowed; } .empty-state { text-align: center; padding: 4rem 2rem; color: #666; } .empty-state h3 { color: #333; margin-bottom: 0.5rem; } .utility-buttons { display: flex; gap: 0.5rem; } .utility-btn { padding: 0.5rem 1rem; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; font-size: 0.9em; } .utility-btn:hover { background: #f5f5f5; } `; connectedCallback() { super.connectedCallback(); this.loadContracts(); } loadContracts() { this.contracts = DSPContractStorage.getAll(); this.applyFilters(); } applyFilters() { let filtered = this.contracts; // Filter by state if (this.filterState !== "ALL") { filtered = filtered.filter((c) => c.state === this.filterState); } // Filter by search query if (this.searchQuery) { const query = this.searchQuery.toLowerCase(); filtered = filtered.filter( (c) => c.assetName.toLowerCase().includes(query) || c.providerName.toLowerCase().includes(query) || c.assetId.toLowerCase().includes(query), ); } this.filteredContracts = filtered; } handleFilterChange(state: ContractState | "ALL") { this.filterState = state; this.applyFilters(); } handleSearch(e: Event) { this.searchQuery = (e.target as HTMLInputElement).value; this.applyFilters(); } async handleAccessResource(contract: StoredContract) { const dspStore = (window as any).dspStore; if (!dspStore) { alert("DSP connector not available. Please ensure the catalogue is loaded first."); return; } const agreementId = contract.agreementId || contract.contractId; if (!agreementId) { alert("No agreement ID available for this contract."); return; } try { // If we already have an EDR token cached, reuse it if (contract.edrToken && contract.edrEndpoint) { const data = await dspStore.fetchWithEDRToken({ endpoint: contract.edrEndpoint, authorization: contract.edrToken, authType: "bearer", type: "https://w3id.org/idsa/v4.1/HTTP", endpointType: "https://w3id.org/idsa/v4.1/HTTP", }); alert(`Data received:\n\n${JSON.stringify(data, null, 2).substring(0, 500)}`); return; } // Initiate a new EDR transfer const transferId = await dspStore.initiateEDRTransfer( contract.assetId, contract.providerAddress, agreementId, contract.providerParticipantId, ); const edrDataAddress = await dspStore.getEDRToken(transferId); if (!edrDataAddress) { throw new Error("Failed to retrieve EDR token"); } // Transform localhost endpoint to public provider address if needed let endpoint = edrDataAddress.endpoint; if (endpoint.includes("localhost") || endpoint.includes("127.0.0.1")) { const providerBase = contract.providerAddress.replace("/protocol", ""); const localUrl = new URL(endpoint); endpoint = `${providerBase}${localUrl.pathname}${localUrl.search}`; } // Cache the EDR token on the contract for reuse DSPContractStorage.update(contract.id, { edrToken: edrDataAddress.authorization, edrEndpoint: endpoint, transferId, }); const data = await dspStore.fetchWithEDRToken({ endpoint, authorization: edrDataAddress.authorization, authType: "bearer", type: "https://w3id.org/idsa/v4.1/HTTP", endpointType: "https://w3id.org/idsa/v4.1/HTTP", }); alert(`Data received:\n\n${JSON.stringify(data, null, 2).substring(0, 500)}`); this.loadContracts(); } catch (error) { console.error("EDR resource access failed:", error); alert(`Failed to access resource: ${(error as Error).message}`); } } handleViewDetails(contract: StoredContract) { this.selectedContract = contract; // TODO: Open modal with full contract details } handleDeleteContract(contract: StoredContract) { if (confirm(`Delete contract for ${contract.assetName}?`)) { DSPContractStorage.delete(contract.id); this.loadContracts(); } } handleExport() { const json = DSPContractStorage.export(); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `dsp-contracts-${new Date().toISOString()}.json`; a.click(); URL.revokeObjectURL(url); } handleImport() { const input = document.createElement("input"); input.type = "file"; input.accept = "application/json"; input.onchange = (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const json = e.target?.result as string; if (DSPContractStorage.import(json)) { this.loadContracts(); alert("Contracts imported successfully!"); } else { alert("Failed to import contracts. Check console for errors."); } }; reader.readAsText(file); } }; input.click(); } handleClearAll() { if (confirm("Delete all contracts? This cannot be undone.")) { DSPContractStorage.clear(); this.loadContracts(); } } render() { const stats = DSPContractStorage.getStats(); return html`

${msg("DSP Contract Catalog")}

${stats.total} ${msg("Total")}
${stats.active} ${msg("Active")}
${stats.byState.REQUESTED + stats.byState.OFFERED} ${msg("Pending")}
${ this.filteredContracts.length === 0 ? html`

${msg("No contracts found")}

${ this.contracts.length === 0 ? msg("Start by negotiating contracts from the catalog.") : msg("Try adjusting your filters or search query.") }

` : html`
${this.filteredContracts.map( (contract) => html`

${contract.assetName}

${contract.assetId}
${contract.state}
${contract.providerName}
${msg("Created")}: ${new Date(contract.createdAt).toLocaleString()}
${ contract.agreedAt ? html`
${msg("Agreed")}: ${new Date(contract.agreedAt).toLocaleString()}
` : "" } ${ contract.finalizedAt ? html`
${msg("Finalized")}: ${new Date(contract.finalizedAt).toLocaleString()}
` : "" } ${ contract.agreementId ? html`
${msg("Agreement ID")}: ${contract.agreementId}
` : "" }
${ contract.state === "FINALIZED" ? html` ` : "" }
`, )}
` } `; } } declare global { interface HTMLElementTagNameMap { "dsp-contract-catalog": DSPContractCatalog; } }