feat: enhance service discovery and proxy management

This commit is contained in:
gabdevele
2026-05-05 13:57:46 +02:00 Unverified
parent 93ba8e9ac2
commit a523c2faa3
53 changed files with 3206 additions and 706 deletions
+581 -72
View File
@@ -7,19 +7,38 @@ import {
watch,
type InjectionKey,
} from "vue";
import { useRoute, useRouter } from "vue-router";
import { api, getToken, login, logout } from "../api";
import { ApiError, api, getToken, login, logout, setToken } from "../api";
import type {
AddonExample,
DockerService,
DockerServiceBinding,
HealthResult,
MitmOption,
ProxyDocument,
ProxyDraft,
} from "../types";
const editorStorageKey = "theplug.editor.open";
const toolStorageKey = "theplug.tool.view";
const optionScopeStorageKey = "theplug.option.scope";
const scanRootStorageKey = "theplug.scan.root";
const focusedMitmOptions = new Set([
"anticache",
"block_global",
"connection_strategy",
"flow_detail",
"http2",
"http3",
"keep_host_header",
"rawtcp",
"showhost",
"ssl_insecure",
"termlog_verbosity",
"upstream_cert",
"validate_inbound_headers",
"websocket",
]);
export const modes = [
"reverse",
@@ -46,7 +65,7 @@ export const protocols = [
];
export type RuntimeAction = "start" | "stop" | "restart";
export type ToolView = "options" | "examples" | "logs";
export type OptionScope = "focused" | "all";
export function createDefaultDraft(port = 8080, code?: string): ProxyDraft {
return {
@@ -82,6 +101,7 @@ def request(flow: http.HTTPFlow) -> None:
service: {
host: "127.0.0.1",
port: 9000,
bind_host: null,
name: null,
folder: null,
upstream_port: null,
@@ -99,13 +119,48 @@ function writeSession(key: string, value: string) {
window.sessionStorage.setItem(key, value);
}
function parseProxyPort(value: unknown): number | null {
const port = Number(value);
return Number.isInteger(port) && port > 0 ? port : null;
}
function parseWizardMode(value: unknown): "create" | "edit" | null {
return value === "create" || value === "edit" ? value : null;
}
function parseWizardStep(value: unknown): number {
const step = Number(value);
return step >= 0 && step <= 2 ? step : 0;
}
function sameQuery(
current: Record<string, unknown>,
expected: Record<string, string | undefined>,
) {
const keys = new Set([...Object.keys(current), ...Object.keys(expected)]);
for (const key of keys) {
const currentValue = current[key];
const normalizedCurrent = Array.isArray(currentValue)
? String(currentValue[0] ?? "")
: typeof currentValue === "string"
? currentValue
: undefined;
if (normalizedCurrent !== expected[key]) return false;
}
return true;
}
export function useControlPlane() {
const router = useRouter();
const route = useRoute();
const token = ref(getToken());
const username = ref("admin");
const password = ref("");
const authMessage = ref("");
const error = ref("");
const notice = ref("");
const loading = ref(false);
const servicesLoading = ref(false);
const proxies = ref<ProxyDocument[]>([]);
const services = ref<DockerService[]>([]);
const options = ref<MitmOption[]>([]);
@@ -113,21 +168,153 @@ export function useControlPlane() {
const selectedPort = ref<number | null>(null);
const health = ref<HealthResult | null>(null);
const logs = ref("");
const logFilter = ref("");
const logsPaused = ref(false);
const optionSearch = ref("");
const exampleSearch = ref("");
const customExampleName = ref("");
const editorOpen = ref(readSession(editorStorageKey, "0") === "1");
const toolView = ref<ToolView>(
readSession(toolStorageKey, "options") as ToolView,
const scanRoot = ref(readSession(scanRootStorageKey, "/root"));
const optionScope = ref<OptionScope>(
readSession(optionScopeStorageKey, "focused") as OptionScope,
);
const selectedOptionName = ref("");
const selectedExampleId = ref("");
const draft = reactive<ProxyDraft>(createDefaultDraft());
const loadedPort = ref<number | null>(null);
const wizardOpen = ref(false);
const wizardMode = ref<"create" | "edit">("create");
const wizardStep = ref(0);
let noticeTimer: number | null = null;
let logPollTimer: number | null = null;
let syncingRoute = false;
const selectedProxy = computed(() =>
proxies.value.find((proxy) => proxy.proxy_port === selectedPort.value),
);
const runtimeStatus = computed(() => selectedProxy.value?.status ?? "draft");
const isRunning = computed(() =>
["running", "starting", "restarting"].includes(runtimeStatus.value),
);
const actionState = computed(() => ({
canSave: !loading.value && !isRunning.value,
canStart: !loading.value && Boolean(selectedProxy.value) && !isRunning.value,
canStop: !loading.value && Boolean(selectedProxy.value) && isRunning.value,
canRestart: !loading.value && Boolean(selectedProxy.value) && isRunning.value,
canDelete: !loading.value && Boolean(selectedProxy.value),
showSave: !isRunning.value,
showStart: Boolean(selectedProxy.value) && !isRunning.value,
showStop: Boolean(selectedProxy.value) && isRunning.value,
showRestart: Boolean(selectedProxy.value) && isRunning.value,
}));
const wizardTitle = computed(() =>
wizardMode.value === "edit" ? "Edit proxy settings" : "Create proxy",
);
const wizardStepTitle = computed(() => {
return ["Proxy route", "Healthchecks", "Custom mitmdump options"][
wizardStep.value
] ?? "Proxy route";
});
const canGoBackWizard = computed(() => wizardStep.value > 0);
const canGoNextWizard = computed(() => wizardStep.value < 2);
const filteredLogs = computed(() => {
const source = logs.value || "";
const query = logFilter.value.trim().toLowerCase();
if (!query) return source;
return source
.split("\n")
.filter((line) => line.toLowerCase().includes(query))
.join("\n");
});
function buildRouteQuery() {
const query: Record<string, string | undefined> = {
scanRoot: scanRoot.value.trim() && scanRoot.value.trim() !== "/root"
? scanRoot.value.trim()
: undefined,
};
if (wizardOpen.value) {
query.modal = wizardMode.value;
query.step = String(wizardStep.value);
}
return query;
}
async function syncRouteState() {
if (syncingRoute) return;
const path = selectedPort.value ? `/proxies/${selectedPort.value}` : "/proxies";
const query = buildRouteQuery();
if (route.path === path && sameQuery(route.query as Record<string, unknown>, query)) {
return;
}
syncingRoute = true;
try {
await router.replace({ path, query });
} finally {
syncingRoute = false;
}
}
function applyRouteState() {
if (syncingRoute) return;
syncingRoute = true;
try {
selectedPort.value = parseProxyPort(route.params.proxyPort);
const routedScanRoot =
typeof route.query.scanRoot === "string" ? route.query.scanRoot : null;
scanRoot.value = routedScanRoot || readSession(scanRootStorageKey, "/root");
const modal = parseWizardMode(route.query.modal);
const step = parseWizardStep(route.query.step);
if (modal === "create") {
prepareDraftForCreate();
wizardMode.value = "create";
wizardStep.value = step;
wizardOpen.value = true;
} else if (modal === "edit" && selectedPort.value !== null) {
wizardMode.value = "edit";
wizardStep.value = step;
wizardOpen.value = true;
} else {
wizardOpen.value = false;
wizardStep.value = 0;
}
} finally {
syncingRoute = false;
}
}
const serviceEntries = computed(() =>
services.value.flatMap((service) =>
bindingsFor(service).map((binding) => {
const proxy =
proxies.value.find(
(item) =>
item.service.name === service.name &&
item.service.folder === service.folder &&
item.service.port === binding.published,
) ?? null;
return {
id: `${service.folder}:${service.name}:${binding.host ?? "*"}:${binding.published}`,
service,
binding,
proxy,
listenHost: binding.host ?? "0.0.0.0",
};
}),
),
);
const missingServiceCount = computed(
() => serviceEntries.value.filter((entry) => !entry.proxy).length,
);
const routeState = computed(() => {
const livePort =
selectedProxy.value?.service.upstream_port ?? draft.service.upstream_port ?? null;
@@ -136,27 +323,43 @@ export function useControlPlane() {
const remapped = livePort !== null && livePort !== draft.service.port;
const configuredTarget = `${draft.service.host}:${draft.service.port}`;
const liveTarget = `${draft.service.host}:${livePort ?? draft.service.port}`;
const takeoverReady =
reverseManaged && dockerManaged && draft.proxy_port === draft.service.port;
const takeoverMismatch =
reverseManaged && dockerManaged && draft.proxy_port !== draft.service.port;
let startNotice = "Save stores configuration only. Start applies runtime routing.";
if (reverseManaged && dockerManaged) {
let startNotice: string | null = null;
if (takeoverReady) {
startNotice =
`Starting this reverse proxy will move the service from :${draft.service.port} ` +
`to a temporary upstream port and bind mitmdump on :${draft.proxy_port}.`;
`Start will move the real service from :${draft.service.port} to a temporary upstream port and put mitmdump on :${draft.proxy_port}.`;
} else if (takeoverMismatch) {
startNotice =
`Start will move the real service away from :${draft.service.port}, but this proxy is listening on :${draft.proxy_port}. Set the proxy port to :${draft.service.port} if you want mitmdump to replace the original route.`;
} else if (draft.mode_spec) {
startNotice =
"This proxy uses a raw mode specification. Start will follow the custom mitmdump mode exactly as configured.";
"Raw mode spec is enabled. Start will use the exact mitmdump mode string you configured.";
} else if (draft.mode !== "reverse") {
startNotice =
"This mode does not use Docker port remapping. Start only launches the configured mitmdump listener.";
"This mode does not remap the Docker service port. Start only launches the configured mitmdump listener.";
}
let liveNotice = "No runtime remap is active.";
let liveNotice: string | null = null;
if (remapped) {
liveNotice = `Live upstream is currently ${liveTarget}. Stop restores :${draft.service.port}.`;
} else if (reverseManaged && dockerManaged) {
liveNotice = "The original service port stays unchanged until Start is executed.";
} else if (takeoverMismatch) {
liveNotice = "The service will be moved away on Start, but the original route is not taken over unless the proxy port matches the service port.";
}
const summaryHint = takeoverMismatch
? startNotice
: remapped
? liveNotice
: draft.mode_spec
? startNotice
: draft.mode !== "reverse"
? startNotice
: null;
return {
configuredListen: `${draft.listen_host}:${draft.proxy_port}`,
configuredTarget,
@@ -167,26 +370,35 @@ export function useControlPlane() {
reverseManaged,
remapped,
livePort,
takeoverReady,
takeoverMismatch,
startNotice,
liveNotice,
summaryHint,
};
});
const filteredOptions = computed(() => {
const query = optionSearch.value.toLowerCase().trim();
const available = options.value.filter((option) => !option.reserved);
const filtered = available
const scoped = query
? available
: optionScope.value === "all"
? available
: available.filter((option) => focusedMitmOptions.has(option.name));
const filtered = scoped
.filter((option) => {
if (!query) return true;
return `${option.name} ${option.description}`.toLowerCase().includes(query);
})
.sort((a, b) => a.name.localeCompare(b.name));
return filtered.slice(0, query ? 80 : 36);
return filtered.slice(0, query ? 80 : optionScope.value === "all" ? 80 : 18);
});
const optionCount = computed(
() => options.value.filter((option) => !option.reserved).length,
);
const activeOptionCount = computed(() => Object.keys(draft.mitm_options).length);
const selectedOption = computed(
() =>
@@ -226,19 +438,43 @@ export function useControlPlane() {
const codeLineCount = computed(() => draft.code.split("\n").length);
watch(selectedProxy, (proxy) => {
if (!proxy) return;
Object.assign(draft, structuredClone(proxy));
health.value = null;
logs.value = "";
watch(
[selectedPort, proxies],
([port]) => {
if (port === null || port === loadedPort.value) return;
const proxy = proxies.value.find((item) => item.proxy_port === port);
if (!proxy) return;
hydrateDraft(proxy);
},
{ immediate: true },
);
watch(
() => [
route.params.proxyPort,
route.query.modal,
route.query.step,
route.query.scanRoot,
],
() => {
applyRouteState();
},
{ immediate: true },
);
watch(
[selectedPort, wizardOpen, wizardMode, wizardStep, scanRoot],
() => {
void syncRouteState();
},
);
watch(optionScope, (value) => {
writeSession(optionScopeStorageKey, value);
});
watch(editorOpen, (value) => {
writeSession(editorStorageKey, value ? "1" : "0");
});
watch(toolView, (value) => {
writeSession(toolStorageKey, value);
watch(scanRoot, (value) => {
writeSession(scanRootStorageKey, value);
});
watch(filteredOptions, (items) => {
@@ -253,60 +489,196 @@ export function useControlPlane() {
}
}, { immediate: true });
watch(selectedProxy, (proxy) => {
if (!proxy) return;
if (!wizardOpen.value) {
hydrateDraft(proxy);
}
});
watch(
[selectedPort, token, logsPaused],
([port, authToken, paused]) => {
stopLogPolling();
if (!authToken || port === null || paused) return;
void syncLogs(true);
if (typeof window === "undefined") return;
logPollTimer = window.setInterval(() => {
void syncLogs(true);
}, 2000);
},
{ immediate: true },
);
async function signIn() {
await run(async () => {
await login(username.value, password.value);
token.value = getToken();
authMessage.value = "";
password.value = "";
await refreshAll();
}, "Logged in");
}
async function signOut() {
await logout();
token.value = null;
proxies.value = [];
if (token.value) {
await logout();
}
clearSession();
}
async function refreshAll() {
await run(async () => {
const [proxyList, serviceList, optionList, exampleList] = await Promise.all([
api.proxies(),
api.services(),
api.options(),
api.examples(),
]);
proxies.value = proxyList;
services.value = serviceList;
options.value = optionList;
examples.value = exampleList;
if (!selectedPort.value && proxyList[0]) {
selectedPort.value = proxyList[0].proxy_port;
servicesLoading.value = true;
try {
const [proxyList, serviceResponse, optionList, exampleList] =
await Promise.all([
api.proxies(),
api.services(scanRoot.value.trim() || undefined),
api.options(),
api.examples(),
]);
proxies.value = proxyList;
services.value = serviceResponse.services;
scanRoot.value = serviceResponse.scan_root;
options.value = optionList;
examples.value = exampleList;
if (!proxyList.some((proxy) => proxy.proxy_port === selectedPort.value)) {
selectedPort.value = null;
}
} finally {
servicesLoading.value = false;
}
});
}
function newProxy() {
async function refreshServices() {
servicesLoading.value = true;
error.value = "";
try {
const response = await api.services(scanRoot.value.trim() || undefined);
scanRoot.value = response.scan_root;
services.value = response.services;
} catch (err) {
error.value = err instanceof Error ? err.message : String(err);
} finally {
servicesLoading.value = false;
}
}
function prepareDraftForCreate(announce = false) {
selectedPort.value = null;
loadedPort.value = null;
health.value = null;
logs.value = "";
logFilter.value = "";
Object.assign(
draft,
createDefaultDraft(nextPort(), examples.value[0]?.code ?? draft.code),
);
notice.value = "New proxy draft created. Save stores configuration only.";
toolView.value = "options";
if (announce) {
notice.value = "Draft created.";
}
}
function newProxy() {
prepareDraftForCreate(true);
}
function openCreateWizard(
service?: DockerService,
binding?: DockerServiceBinding,
) {
prepareDraftForCreate();
if (service && binding) {
Object.assign(draft, draftForService(service, binding));
}
wizardMode.value = "create";
wizardStep.value = 0;
wizardOpen.value = true;
void syncRouteState();
}
function openEditWizard(proxy = selectedProxy.value) {
if (!proxy) return;
selectedPort.value = proxy.proxy_port;
hydrateDraft(proxy);
wizardMode.value = "edit";
wizardStep.value = 0;
wizardOpen.value = true;
void syncRouteState();
}
function closeWizard() {
if (wizardMode.value === "edit" && selectedProxy.value) {
hydrateDraft(selectedProxy.value);
} else if (wizardMode.value === "create") {
prepareDraftForCreate();
}
wizardOpen.value = false;
wizardStep.value = 0;
void syncRouteState();
}
function nextWizardStep() {
wizardStep.value = Math.min(wizardStep.value + 1, 2);
void syncRouteState();
}
function previousWizardStep() {
wizardStep.value = Math.max(wizardStep.value - 1, 0);
void syncRouteState();
}
async function createProxyFromService(
service: DockerService,
binding: DockerServiceBinding,
) {
openCreateWizard(service, binding);
}
async function createMissingServiceProxies() {
await run(async () => {
for (const entry of serviceEntries.value) {
if (entry.proxy) continue;
await api.createProxy(draftForService(entry.service, entry.binding));
}
await refreshAll();
const first = proxies.value[0];
if (first) {
selectedPort.value = first.proxy_port;
hydrateDraft(first);
}
}, "Missing proxies created");
}
async function saveProxy() {
let persisted = false;
await run(async () => {
const payload = cleanDraft();
const saved = selectedProxy.value
? await api.updateProxy(selectedProxy.value.proxy_port, payload)
? await api.updateProxy(selectedProxy.value.proxy_port, {
name: payload.name,
code: payload.code,
mode: payload.mode,
mode_spec: payload.mode_spec,
protocol: payload.protocol,
listen_host: payload.listen_host,
mitm_options: payload.mitm_options,
healthcheck: payload.healthcheck,
service: payload.service,
})
: await api.createProxy(payload);
await refreshAll();
selectedPort.value = saved.proxy_port;
const refreshed = proxies.value.find((proxy) => proxy.proxy_port === saved.proxy_port);
hydrateDraft(refreshed ?? saved);
persisted = true;
}, "Proxy saved");
if (persisted) {
wizardOpen.value = false;
wizardStep.value = 0;
}
}
async function removeProxy() {
@@ -315,6 +687,7 @@ export function useControlPlane() {
await run(async () => {
await api.deleteProxy(proxy.proxy_port);
selectedPort.value = null;
loadedPort.value = null;
await refreshAll();
}, "Proxy deleted");
}
@@ -330,8 +703,10 @@ export function useControlPlane() {
: kind === "stop"
? await api.stop(port)
: await api.restart(port);
replaceProxy(updated);
await refreshAll();
selectedPort.value = updated.proxy_port;
const refreshed = proxies.value.find((item) => item.proxy_port === updated.proxy_port);
hydrateDraft(refreshed ?? updated);
}, `Proxy ${kind} requested`);
}
@@ -344,11 +719,19 @@ export function useControlPlane() {
}
async function loadLogs() {
await syncLogs(false);
}
async function syncLogs(silent = false) {
const proxy = selectedProxy.value;
if (!proxy) return;
await run(async () => {
try {
logs.value = (await api.logs(proxy.proxy_port)).log;
});
} catch (err) {
if (!silent) {
error.value = err instanceof Error ? err.message : String(err);
}
}
}
async function saveCurrentAsExample() {
@@ -364,7 +747,7 @@ export function useControlPlane() {
await api.createExample({
id,
name,
description: `Captured from ${draft.mode} ${draft.protocol} proxy ${draft.name}.`,
description: `Saved from ${draft.name} (${draft.mode} / ${draft.protocol}).`,
category: "custom",
code: draft.code,
source_url: null,
@@ -383,19 +766,38 @@ export function useControlPlane() {
}, "Custom example deleted");
}
function applyService(service: DockerService, port: number) {
function applyService(
service: DockerService,
port: number,
bindHost: string | null = null,
) {
const shouldFollowServicePort =
!selectedProxy.value || draft.proxy_port === draft.service.port;
draft.service.name = service.name;
draft.service.folder = service.folder;
draft.service.port = port;
draft.service.bind_host = bindHost;
draft.service.host = "127.0.0.1";
draft.listen_host = bindHost ?? "0.0.0.0";
if (draft.mode === "reverse" && !draft.mode_spec && shouldFollowServicePort) {
draft.proxy_port = port;
}
}
function applyExample(example: AddonExample) {
if (
draft.code.trim() &&
draft.code.trim() !== example.code.trim() &&
typeof window !== "undefined" &&
!window.confirm(
"Applying this template will overwrite your current proxy script. Continue?",
)
) {
return;
}
selectedExampleId.value = example.id;
draft.code = example.code;
notice.value = editorOpen.value
? `Loaded example: ${example.name}`
: `Loaded example: ${example.name}. Open the editor to review or modify the code.`;
notice.value = `Loaded ${example.name}.`;
}
function selectExample(exampleId: string) {
@@ -436,12 +838,20 @@ export function useControlPlane() {
draft.mitm_options[name] = value;
}
function toggleEditor(force?: boolean) {
editorOpen.value = typeof force === "boolean" ? force : !editorOpen.value;
function matchProxyPortToService() {
draft.proxy_port = draft.service.port;
}
function setToolView(view: ToolView) {
toolView.value = view;
function toggleOptionScope(scope: OptionScope) {
optionScope.value = scope;
}
function toggleLogsPaused() {
logsPaused.value = !logsPaused.value;
}
function clearLogs() {
logs.value = "";
}
function cleanDraft(): ProxyDraft {
@@ -463,6 +873,7 @@ export function useControlPlane() {
service: {
host: draft.service.host,
port: draft.service.port,
bind_host: draft.service.bind_host || null,
name: draft.service.name || null,
folder: draft.service.folder || null,
upstream_port: draft.service.upstream_port || null,
@@ -471,12 +882,35 @@ export function useControlPlane() {
) as ProxyDraft;
}
function replaceProxy(proxy: ProxyDocument) {
const index = proxies.value.findIndex(
(item) => item.proxy_port === proxy.proxy_port,
);
if (index === -1) proxies.value.push(proxy);
else proxies.value[index] = proxy;
function hydrateDraft(proxy: ProxyDocument) {
Object.assign(draft, JSON.parse(JSON.stringify(proxy)) as ProxyDraft);
loadedPort.value = proxy.proxy_port;
health.value = null;
logs.value = "";
}
function bindingsFor(service: DockerService) {
return service.bindings?.length
? service.bindings
: service.ports.map((published) => ({ published, host: null }));
}
function draftForService(service: DockerService, binding: DockerServiceBinding) {
const template = createDefaultDraft(binding.published, examples.value[0]?.code);
const portSuffix = bindingsFor(service).length > 1 ? `-${binding.published}` : "";
return {
...template,
name: `${service.name}${portSuffix}`,
listen_host: binding.host ?? "0.0.0.0",
service: {
host: "127.0.0.1",
port: binding.published,
bind_host: binding.host ?? null,
name: service.name,
folder: service.folder,
upstream_port: null,
},
} satisfies ProxyDraft;
}
function nextPort() {
@@ -497,26 +931,83 @@ export function useControlPlane() {
async function run(task: () => Promise<void>, success?: string) {
loading.value = true;
error.value = "";
notice.value = "";
clearNotice();
try {
await task();
if (success) notice.value = success;
if (success) showNotice(success);
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
clearSession("Session expired. Sign in again.");
return;
}
error.value = err instanceof Error ? err.message : String(err);
} finally {
loading.value = false;
}
}
function showNotice(message: string, timeout = 2600) {
notice.value = message;
if (typeof window === "undefined") return;
if (noticeTimer !== null) window.clearTimeout(noticeTimer);
noticeTimer = window.setTimeout(() => {
notice.value = "";
noticeTimer = null;
}, timeout);
}
function clearNotice() {
notice.value = "";
if (typeof window === "undefined" || noticeTimer === null) return;
window.clearTimeout(noticeTimer);
noticeTimer = null;
}
function clearSession(message = "") {
stopLogPolling();
setToken(null);
token.value = null;
proxies.value = [];
services.value = [];
options.value = [];
examples.value = [];
selectedPort.value = null;
loadedPort.value = null;
health.value = null;
logs.value = "";
logFilter.value = "";
wizardOpen.value = false;
wizardStep.value = 0;
wizardMode.value = "create";
Object.assign(draft, createDefaultDraft());
authMessage.value = message;
void router.replace({ path: "/proxies", query: {} });
}
function stopLogPolling() {
if (typeof window === "undefined" || logPollTimer === null) return;
window.clearInterval(logPollTimer);
logPollTimer = null;
}
return proxyRefs({
activeOptionCount,
activeOptions,
applyExample,
applyService,
actionState,
authMessage,
checkHealth,
clearLogs,
closeWizard,
codeLineCount,
filteredLogs,
logFilter,
logsPaused,
createMissingServiceProxies,
createProxyFromService,
customExampleName,
draft,
editorOpen,
error,
exampleSearch,
filteredExamples,
@@ -527,15 +1018,21 @@ export function useControlPlane() {
logs,
newProxy,
notice,
openCreateWizard,
openEditWizard,
optionCount,
optionScope,
optionSearch,
password,
previousWizardStep,
proxies,
refreshServices,
refreshAll,
removeExample,
removeOption,
removeProxy,
routeState,
runtimeStatus,
runtimeAction,
saveCurrentAsExample,
saveProxy,
@@ -545,16 +1042,28 @@ export function useControlPlane() {
selectedPort,
selectedProxy,
selectOption,
serviceEntries,
services,
setOption,
setToolView,
signIn,
signOut,
toggleEditor,
matchProxyPortToService,
missingServiceCount,
nextWizardStep,
toggleOptionScope,
toggleLogsPaused,
token,
toolView,
updateOption,
username,
wizardMode,
wizardOpen,
wizardStep,
wizardStepTitle,
wizardTitle,
canGoBackWizard,
canGoNextWizard,
scanRoot,
servicesLoading,
});
}