Files
feuerwerk/src/editor/plugins/PluginsPane.ts
T

155 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Checkbox, HStack, Label, ScrollContainer, Spacer, Theme, VStack } from '../../ui';
import { TabHeader } from '../TabHeader';
import type { PluginManager, PluginRecord } from './loader';
const NAME_WIDTH = 200;
const VERSION_WIDTH = 70;
const FILE_WIDTH = 200;
/** What one plugin's row says about its state, beside the switch. */
export function statusText(record: PluginRecord): string {
if (record.status === 'failed') return `Failed: ${record.error ?? 'unknown reason'}`;
if (record.error) return `Switched off, but: ${record.error}`;
return record.status === 'active' ? 'Active' : 'Off';
}
/** The name to show: known once the plugin has been active in this session, the file name until then. */
export function displayName(record: PluginRecord): string {
return record.meta?.name ?? '—';
}
class PluginRow extends HStack {
readonly switch: Checkbox;
private readonly nameLabel: Label;
private readonly version: Label;
private readonly file: Label;
private readonly status: Label;
constructor(
public record: PluginRecord,
onToggle: (active: boolean) => void,
) {
super({ gap: Theme.gap, padding: Theme.padding, align: 'center' });
this.minHeight = Theme.rowHeight + Theme.padding;
this.switch = new Checkbox(record.status !== 'inactive');
this.switch.onChange = onToggle;
this.add(this.switch);
this.nameLabel = new Label('', { width: NAME_WIDTH, style: 'bodyBold' });
this.version = new Label('', { width: VERSION_WIDTH, style: 'muted' });
this.file = new Label('', { width: FILE_WIDTH, style: 'muted' });
this.status = new Label('', { style: 'small' });
this.add(this.nameLabel);
this.add(this.version);
this.add(this.file);
this.add(this.status, 1);
this.show(record);
}
show(record: PluginRecord): void {
this.record = record;
this.switch.setChecked(record.status !== 'inactive');
this.nameLabel.setText(displayName(record));
this.version.setText(record.meta?.version ?? '');
this.file.setText(record.file);
this.status.setText(statusText(record));
this.status.setStyle(record.status === 'failed' ? 'warning' : 'small');
}
}
/**
* The Plugins tab (#114): every file in `data/plugins/` with a switch. Switching one on loads and
* activates it there and then, switching it off takes its tabs and listeners away again – the
* state is the server's, so it holds for everyone who opens this editor.
*
* A plugin that is off is never loaded, which is why its row has no name until it has run once:
* reading a name out of it would mean running it.
*/
export class PluginsPane extends VStack {
private readonly hint: Label;
private readonly rows = new VStack();
private readonly scroll = new ScrollContainer();
private readonly empty: Label;
private manager: PluginManager | null = null;
private unsubscribe: (() => void) | null = null;
constructor() {
super();
const header = new TabHeader();
header.add(new Label('Plugins', { style: 'bodyBold' }));
header.add(new Spacer(Theme.padding));
this.hint = new Label('', { style: 'small' });
header.add(this.hint, 1);
this.add(header);
this.empty = new Label('Loading…', { style: 'muted', padding: Theme.padding });
this.rows.add(this.empty);
this.scroll.setContent(this.rows);
this.add(this.scroll, 1);
}
setManager(manager: PluginManager): void {
this.unsubscribe?.();
this.manager = manager;
this.unsubscribe = manager.onChange(() => this.refresh());
this.refresh();
}
/** The list could not even be read; there is nothing to show but why. */
showError(message: string): void {
this.manager = null;
this.empty.setText(message);
this.refresh();
}
/** The rows as drawn, and the switch to click – what the check scripts read. */
debugInfo(): {
visible: boolean;
rows: { file: string; name: string; status: string; checked: boolean; text: string }[];
} {
return {
visible: this.visible,
rows: this.rowWidgets().map((row) => ({
file: row.record.file,
name: displayName(row.record),
status: row.record.status,
checked: row.switch.checked,
text: statusText(row.record),
})),
};
}
/** The switch of one plugin's row, for the check scripts; null when it has no row. */
switchOf(file: string): Checkbox | null {
return this.rowWidgets().find((row) => row.record.file === file)?.switch ?? null;
}
private rowWidgets(): PluginRow[] {
return this.rows.children.filter((child): child is PluginRow => child instanceof PluginRow);
}
private refresh(): void {
const records = this.manager?.records ?? [];
const rows = this.rowWidgets();
const sameList =
rows.length === records.length && rows.every((row, i) => row.record.file === records[i]?.file);
if (sameList) {
rows.forEach((row, i) => row.show(records[i]!));
} else {
for (const row of rows) row.destroy({ children: true });
for (const record of records) {
const row = new PluginRow(record, (active) => void this.manager?.setActive(record.file, active));
this.rows.add(row);
}
}
const nothing = records.length === 0;
this.empty.visible = nothing;
if (nothing && this.manager) this.empty.setText('No plugins in data/plugins');
this.hint.setText(
nothing
? ''
: 'Loaded from data/plugins, for everyone using this server. A new file stays off until switched on here.',
);
this.scroll.relayoutContent();
this.relayout();
}
}