feat: implement URL plugins with table/row actions and prefill support

- Add URL plugin effects (register, export_url, clear_urls) in core
- Add PluginHostnameInfo, HostnameMetadata::Plugin, and plugin registration types
- Implement plugin URL table in web UI with tableAction button and rowAction overflow menus
- Thread urlPluginMetadata (packageId, hostId, interfaceId, internalPort) as prefill to actions
- Add prefill support to PackageActionData so metadata passes through form dialogs
- Add i18n translations for plugin error messages
- Clean up plugin URLs on package uninstall
This commit is contained in:
Aiden McClelland
2026-02-18 17:51:13 -07:00
parent dce975410f
commit 9c3053f103
53 changed files with 792 additions and 278 deletions

View File

@@ -6,15 +6,9 @@ import {
ActionInfo,
Actions,
} from '../../base/lib/actions/setupActions'
import {
SyncOptions,
ServiceInterfaceId,
PackageId,
ServiceInterfaceType,
Effects,
} from '../../base/lib/types'
import { ServiceInterfaceType, Effects } from '../../base/lib/types'
import * as patterns from '../../base/lib/util/patterns'
import { BackupSync, Backups } from './backup/Backups'
import { Backups } from './backup/Backups'
import { smtpInputSpec } from '../../base/lib/actions/input/inputSpecConstants'
import { Daemon, Daemons } from './mainFn/Daemons'
import { checkPortListening } from './health/checkFns/checkPortListening'
@@ -25,6 +19,7 @@ import { setupMain } from './mainFn'
import { defaultTrigger } from './trigger/defaultTrigger'
import { changeOnFirstSuccess, cooldownTrigger } from './trigger'
import { setupServiceInterfaces } from '../../base/lib/interfaces/setupInterfaces'
import { setupExportedUrls } from '../../base/lib/interfaces/setupExportedUrls'
import { successFailure } from './trigger/successFailure'
import { MultiHost, Scheme } from '../../base/lib/interfaces/Host'
import { ServiceInterfaceBuilder } from '../../base/lib/interfaces/ServiceInterfaceBuilder'
@@ -85,8 +80,16 @@ export class StartSdk<Manifest extends T.SDKManifest> {
return new StartSdk<Manifest>(manifest)
}
private ifPluginEnabled<P extends T.PluginId, T>(
plugin: P,
value: T,
): Manifest extends { plugins: P[] } ? T : null {
if (this.manifest.plugins?.includes(plugin)) return value as any
return null as any
}
build(isReady: AnyNeverCond<[Manifest], 'Build not ready', true>) {
type NestedEffects = 'subcontainer' | 'store' | 'action'
type NestedEffects = 'subcontainer' | 'store' | 'action' | 'plugin'
type InterfaceEffects =
| 'getServiceInterface'
| 'listServiceInterfaces'
@@ -172,7 +175,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
},
checkDependencies: checkDependencies as <
DependencyId extends keyof Manifest['dependencies'] &
PackageId = keyof Manifest['dependencies'] & PackageId,
T.PackageId = keyof Manifest['dependencies'] & T.PackageId,
>(
effects: Effects,
packageIds?: DependencyId[],
@@ -737,6 +740,48 @@ export class StartSdk<Manifest extends T.SDKManifest> {
List,
Value,
Variants,
plugin: {
url: this.ifPluginEnabled('url-v0' as const, {
register: (
effects: T.Effects,
options: {
tableAction: ActionInfo<
T.ActionId,
{
urlPluginMetadata: {
packageId: T.PackageId
interfaceId: T.ServiceInterfaceId
hostId: T.HostId
internalPort: number
}
}
>
},
) =>
effects.plugin.url.register({
tableAction: options.tableAction.id,
}),
exportUrl: (
effects: T.Effects,
options: {
hostnameInfo: T.PluginHostnameInfo
rowActions: ActionInfo<
T.ActionId,
{
urlPluginMetadata: T.PluginHostnameInfo & {
interfaceId: T.ServiceInterfaceId
}
}
>[]
},
) =>
effects.plugin.url.exportUrl({
hostnameInfo: options.hostnameInfo,
rowActions: options.rowActions.map((a) => a.id),
}),
setupExportedUrls, // similar to setupInterfaces
}),
},
}
}
}

View File

@@ -89,5 +89,6 @@ export function buildManifest<
),
},
hardwareAcceleration: manifest.hardwareAcceleration ?? false,
plugins: manifest.plugins ?? [],
}
}