feat(ui): refresh scm views from live events
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
676130ab84
commit
32e65b8b43
8 changed files with 2095 additions and 26 deletions
0
pkg/api/ui/dist/.gitkeep
vendored
0
pkg/api/ui/dist/.gitkeep
vendored
2007
pkg/api/ui/dist/core-scm.js
vendored
Normal file
2007
pkg/api/ui/dist/core-scm.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -162,6 +162,10 @@ export class ScmInstalled extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.loadInstalled();
|
||||
}
|
||||
|
||||
private async handleUpdate(code: string) {
|
||||
this.updating = new Set([...this.updating, code]);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -265,6 +265,11 @@ export class ScmManifest extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.verifyResult = null;
|
||||
await this.loadManifest();
|
||||
}
|
||||
|
||||
private async handleVerify() {
|
||||
if (!this.verifyKey.trim()) return;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -219,6 +219,10 @@ export class ScmMarketplace extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.loadModules();
|
||||
}
|
||||
|
||||
private handleSearch(e: Event) {
|
||||
this.searchQuery = (e.target as HTMLInputElement).value;
|
||||
this.loadModules();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import type { PropertyValues } from 'lit';
|
||||
import { connectScmEvents, type ScmEvent } from './shared/events.js';
|
||||
|
||||
// Side-effect imports to register child elements
|
||||
|
|
@ -11,6 +12,9 @@ import './scm-manifest.js';
|
|||
import './scm-registry.js';
|
||||
|
||||
type TabId = 'marketplace' | 'installed' | 'manifest' | 'registry';
|
||||
type RefreshableElement = HTMLElement & {
|
||||
refresh?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
/**
|
||||
* <core-scm-panel> — Top-level HLCRF panel with tabs.
|
||||
|
|
@ -154,18 +158,28 @@ export class ScmPanel extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('wsUrl') && this.isConnected) {
|
||||
this.connectWs();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.disconnectWs();
|
||||
}
|
||||
|
||||
private connectWs() {
|
||||
this.disconnectWs();
|
||||
if (!this.wsUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ws = connectScmEvents(this.wsUrl, (event: ScmEvent) => {
|
||||
this.lastEvent = event.channel ?? event.type ?? '';
|
||||
this.requestUpdate();
|
||||
this.refreshForEvent(event);
|
||||
});
|
||||
this.ws.onopen = () => {
|
||||
this.wsConnected = true;
|
||||
|
|
@ -175,27 +189,55 @@ export class ScmPanel extends LitElement {
|
|||
};
|
||||
}
|
||||
|
||||
private disconnectWs() {
|
||||
if (!this.ws) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
private handleTabClick(tab: TabId) {
|
||||
this.activeTab = tab;
|
||||
}
|
||||
|
||||
private handleRefresh() {
|
||||
// Force re-render of active child by toggling a key
|
||||
const content = this.shadowRoot?.querySelector('.content');
|
||||
if (content) {
|
||||
const child = content.firstElementChild;
|
||||
if (child && 'loadModules' in child) {
|
||||
(child as any).loadModules();
|
||||
} else if (child && 'loadInstalled' in child) {
|
||||
(child as any).loadInstalled();
|
||||
} else if (child && 'loadManifest' in child) {
|
||||
(child as any).loadManifest();
|
||||
} else if (child && 'loadRegistry' in child) {
|
||||
(child as any).loadRegistry();
|
||||
}
|
||||
private async handleRefresh() {
|
||||
await this.refreshActiveTab();
|
||||
}
|
||||
|
||||
private refreshForEvent(event: ScmEvent) {
|
||||
const targets = this.tabsForChannel(event.channel ?? event.type ?? '');
|
||||
if (targets.includes(this.activeTab)) {
|
||||
void this.refreshActiveTab();
|
||||
}
|
||||
}
|
||||
|
||||
private tabsForChannel(channel: string): TabId[] {
|
||||
if (channel.startsWith('scm.marketplace.')) {
|
||||
return ['marketplace', 'installed'];
|
||||
}
|
||||
if (channel.startsWith('scm.installed.')) {
|
||||
return ['installed'];
|
||||
}
|
||||
if (channel === 'scm.manifest.verified') {
|
||||
return ['manifest'];
|
||||
}
|
||||
if (channel === 'scm.registry.changed') {
|
||||
return ['registry'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private async refreshActiveTab() {
|
||||
const child = this.shadowRoot?.querySelector('.content > *') as RefreshableElement | null;
|
||||
if (!child?.refresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
await child.refresh();
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
switch (this.activeTab) {
|
||||
case 'marketplace':
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ export class ScmRegistry extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
await this.loadRegistry();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html`<div class="loading">Loading registry\u2026</div>`;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ export class ScmApi {
|
|||
|
||||
private async request<T>(path: string, opts?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${this.base}${path}`, opts);
|
||||
const json = await res.json();
|
||||
if (!json.success) {
|
||||
throw new Error(json.error?.message ?? 'Request failed');
|
||||
const json = await res.json().catch(() => null);
|
||||
if (!res.ok) {
|
||||
throw new Error(json?.error?.message ?? `Request failed (${res.status})`);
|
||||
}
|
||||
if (!json?.success) {
|
||||
throw new Error(json?.error?.message ?? 'Request failed');
|
||||
}
|
||||
return json.data as T;
|
||||
}
|
||||
|
|
@ -28,15 +31,15 @@ export class ScmApi {
|
|||
}
|
||||
|
||||
marketplaceItem(code: string) {
|
||||
return this.request<any>(`/marketplace/${code}`);
|
||||
return this.request<any>(`/marketplace/${encodeURIComponent(code)}`);
|
||||
}
|
||||
|
||||
install(code: string) {
|
||||
return this.request<any>(`/marketplace/${code}/install`, { method: 'POST' });
|
||||
return this.request<any>(`/marketplace/${encodeURIComponent(code)}/install`, { method: 'POST' });
|
||||
}
|
||||
|
||||
remove(code: string) {
|
||||
return this.request<any>(`/marketplace/${code}`, { method: 'DELETE' });
|
||||
return this.request<any>(`/marketplace/${encodeURIComponent(code)}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
installed() {
|
||||
|
|
@ -44,7 +47,7 @@ export class ScmApi {
|
|||
}
|
||||
|
||||
updateInstalled(code: string) {
|
||||
return this.request<any>(`/installed/${code}/update`, { method: 'POST' });
|
||||
return this.request<any>(`/installed/${encodeURIComponent(code)}/update`, { method: 'POST' });
|
||||
}
|
||||
|
||||
manifest() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue