/**
* 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`
${
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.state === "FINALIZED"
? html`
`
: ""
}
`,
)}
`
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"dsp-contract-catalog": DSPContractCatalog;
}
}