mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
Merge pull request #395 from eyerrock/container-searchbar
search bar for Docker container list
This commit is contained in:
commit
093ed9c212
@ -10,17 +10,27 @@
|
|||||||
<body>
|
<body>
|
||||||
<br />
|
<br />
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="field">
|
<div class="ui form">
|
||||||
<div class="ui checkbox">
|
<div class="field">
|
||||||
<input type="checkbox" id="showUnexposed" class="hidden" />
|
<input
|
||||||
<label for="showUnexposed"
|
id="searchbar"
|
||||||
>Show Containers with Unexposed Ports
|
type="text"
|
||||||
<br />
|
placeholder="Search..."
|
||||||
<small
|
autocomplete="off"
|
||||||
>Please make sure Zoraxy and the target container share a
|
/>
|
||||||
network</small
|
</div>
|
||||||
>
|
<div class="field">
|
||||||
</label>
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="showUnexposed" class="hidden" />
|
||||||
|
<label for="showUnexposed"
|
||||||
|
>Show Containers with unexposed ports
|
||||||
|
<br />
|
||||||
|
<small
|
||||||
|
>Please make sure Zoraxy and the target container share a
|
||||||
|
network</small
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
@ -46,131 +56,185 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let lines = {};
|
// debounce function to prevent excessive calls to a function
|
||||||
let linesAdded = {};
|
function debounce(func, delay) {
|
||||||
|
let timeout;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
document
|
// wait until DOM is fully loaded before executing script
|
||||||
.getElementById("showUnexposed")
|
$(document).ready(() => {
|
||||||
.addEventListener("change", () => {
|
const $containersList = $("#containersList");
|
||||||
console.log("showUnexposed", $("#showUnexposed").is(":checked"));
|
const $containersAddedList = $("#containersAddedList");
|
||||||
$("#containersList").html('<div class="ui loader active"></div>');
|
const $containersAddedListHeader = $("#containersAddedListHeader");
|
||||||
|
const $searchbar = $("#searchbar");
|
||||||
|
const $showUnexposed = $("#showUnexposed");
|
||||||
|
|
||||||
$("#containersAddedList").empty();
|
let lines = {};
|
||||||
$("#containersAddedListHeader").attr("hidden", true);
|
let linesAdded = {};
|
||||||
|
|
||||||
|
// load showUnexposed checkbox state from local storage
|
||||||
|
function loadShowUnexposedState() {
|
||||||
|
const storedState = localStorage.getItem("showUnexposed");
|
||||||
|
if (storedState !== null) {
|
||||||
|
$showUnexposed.prop("checked", storedState === "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save showUnexposed checkbox state to local storage
|
||||||
|
function saveShowUnexposedState() {
|
||||||
|
localStorage.setItem("showUnexposed", $showUnexposed.prop("checked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch docker containers
|
||||||
|
function getDockerContainers() {
|
||||||
|
$containersList.html('<div class="ui loader active"></div>');
|
||||||
|
$containersAddedList.empty();
|
||||||
|
$containersAddedListHeader.attr("hidden", true);
|
||||||
|
|
||||||
lines = {};
|
lines = {};
|
||||||
linesAdded = {};
|
linesAdded = {};
|
||||||
|
|
||||||
|
const hostRequest = $.get("/api/proxy/list?type=host");
|
||||||
|
const dockerRequest = $.get("/api/docker/containers");
|
||||||
|
|
||||||
|
Promise.all([hostRequest, dockerRequest])
|
||||||
|
.then(([hostData, dockerData]) => {
|
||||||
|
if (!hostData.error && !dockerData.error) {
|
||||||
|
processDockerData(hostData, dockerData);
|
||||||
|
} else {
|
||||||
|
showError(hostData.error || dockerData.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
parent.msgbox("Error loading data: " + error.message, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// process docker data and update ui
|
||||||
|
function processDockerData(hostData, dockerData) {
|
||||||
|
const { containers } = dockerData;
|
||||||
|
const existingTargets = new Set(
|
||||||
|
hostData.flatMap(({ ActiveOrigins }) =>
|
||||||
|
ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
containers.forEach((container) => {
|
||||||
|
const name = container.Names[0].replace(/^\//, "");
|
||||||
|
container.Ports.forEach((portObject) => {
|
||||||
|
let port = portObject.PublicPort || portObject.PrivatePort;
|
||||||
|
if (!portObject.PublicPort && !$showUnexposed.is(":checked"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// if port is not exposed, use container's name and let docker handle the routing
|
||||||
|
// BUT this will only work if the container is on the same network as Zoraxy
|
||||||
|
const targetAddress = portObject.IP || name;
|
||||||
|
const key = `${name}-${port}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingTargets.has(`${targetAddress}:${port}`) &&
|
||||||
|
!linesAdded[key]
|
||||||
|
) {
|
||||||
|
linesAdded[key] = { name, ip: targetAddress, port };
|
||||||
|
} else if (!lines[key]) {
|
||||||
|
lines[key] = { name, ip: targetAddress, port };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
updateContainersList();
|
||||||
|
updateAddedContainersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update containers list
|
||||||
|
function updateContainersList() {
|
||||||
|
$containersList.empty();
|
||||||
|
Object.entries(lines).forEach(([key, line]) => {
|
||||||
|
$containersList.append(`
|
||||||
|
<div class="item">
|
||||||
|
<div class="right floated content">
|
||||||
|
<div class="ui button add-button" data-key="${key}">Add</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">${line.name}</div>
|
||||||
|
<div class="description">${line.ip}:${line.port}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
$containersList.find(".loader").removeClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the added containers list
|
||||||
|
function updateAddedContainersList() {
|
||||||
|
Object.entries(linesAdded).forEach(([key, line]) => {
|
||||||
|
$containersAddedList.append(`
|
||||||
|
<div class="item">
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">${line.name}</div>
|
||||||
|
<div class="description">${line.ip}:${line.port}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
if (Object.keys(linesAdded).length) {
|
||||||
|
$containersAddedListHeader.removeAttr("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// show error message
|
||||||
|
function showError(error) {
|
||||||
|
$containersList.html(
|
||||||
|
`<div class="ui basic segment"><i class="ui red times icon"></i> ${error}</div>`
|
||||||
|
);
|
||||||
|
parent.msgbox(`Error loading data: ${error}`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// event listeners
|
||||||
|
//
|
||||||
|
|
||||||
|
$showUnexposed.on("change", () => {
|
||||||
|
saveShowUnexposedState(); // save the new state to local storage
|
||||||
getDockerContainers();
|
getDockerContainers();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDockerContainers() {
|
$searchbar.on(
|
||||||
const hostRequest = $.get("/api/proxy/list?type=host");
|
"input",
|
||||||
const dockerRequest = $.get("/api/docker/containers");
|
debounce(() => {
|
||||||
|
// debounce searchbar input with 300ms delay, then filter list
|
||||||
|
// this prevents excessive calls to the filter function
|
||||||
|
const search = $searchbar.val().toLowerCase();
|
||||||
|
$("#containersList .item").each((index, item) => {
|
||||||
|
const content = $(item).text().toLowerCase();
|
||||||
|
$(item).toggle(content.includes(search));
|
||||||
|
});
|
||||||
|
}, 300)
|
||||||
|
);
|
||||||
|
|
||||||
Promise.all([hostRequest, dockerRequest])
|
$containersList.on("click", ".add-button", (event) => {
|
||||||
.then(([hostData, dockerData]) => {
|
const key = $(event.currentTarget).data("key");
|
||||||
if (!dockerData.error && !hostData.error) {
|
if (lines[key]) {
|
||||||
const { containers, network } = dockerData;
|
parent.addContainerItem(lines[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const existingTargets = new Set(
|
//
|
||||||
hostData.flatMap(({ ActiveOrigins }) =>
|
// initial calls
|
||||||
ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
|
//
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const container of containers) {
|
// load state of showUnexposed checkbox
|
||||||
const Ports = container.Ports;
|
loadShowUnexposedState();
|
||||||
const name = container.Names[0].replace(/^\//, "");
|
|
||||||
|
|
||||||
for (const portObject of Ports) {
|
// initial load of docker containers
|
||||||
let port = portObject.PublicPort;
|
getDockerContainers();
|
||||||
if (!port) {
|
});
|
||||||
if (!$("#showUnexposed").is(":checked")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
port = portObject.PrivatePort;
|
|
||||||
}
|
|
||||||
const key = `${name}-${port}`;
|
|
||||||
|
|
||||||
// if port is not exposed, use container's name and let docker handle the routing
|
|
||||||
// BUT this will only work if the container is on the same network as Zoraxy
|
|
||||||
const targetAddress = portObject.IP || name;
|
|
||||||
|
|
||||||
if (
|
|
||||||
existingTargets.has(`${targetAddress}:${port}`) &&
|
|
||||||
!linesAdded[key]
|
|
||||||
) {
|
|
||||||
linesAdded[key] = {
|
|
||||||
name,
|
|
||||||
ip: targetAddress,
|
|
||||||
port,
|
|
||||||
};
|
|
||||||
} else if (!lines[key]) {
|
|
||||||
lines[key] = {
|
|
||||||
name,
|
|
||||||
ip: targetAddress,
|
|
||||||
port,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, line] of Object.entries(lines)) {
|
|
||||||
$("#containersList").append(
|
|
||||||
`<div class="item">
|
|
||||||
<div class="right floated content">
|
|
||||||
<div class="ui button" onclick="addContainerItem('${key}');">Add</div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="header">${line.name}</div>
|
|
||||||
<div class="description">
|
|
||||||
${line.ip}:${line.port}
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, line] of Object.entries(linesAdded)) {
|
|
||||||
$("#containersAddedList").append(
|
|
||||||
`<div class="item">
|
|
||||||
<div class="content">
|
|
||||||
<div class="header">${line.name}</div>
|
|
||||||
<div class="description">
|
|
||||||
${line.ip}:${line.port}
|
|
||||||
</div>
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(linesAdded).length &&
|
|
||||||
$("#containersAddedListHeader").removeAttr("hidden");
|
|
||||||
$("#containersList .loader").removeClass("active");
|
|
||||||
} else {
|
|
||||||
parent.msgbox(
|
|
||||||
`Error loading data: ${dockerData.error || hostData.error}`,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
$("#containersList").html(
|
|
||||||
`<div class="ui basic segment"><i class="ui red times icon"></i> ${
|
|
||||||
dockerData.error || hostData.error
|
|
||||||
}</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error.responseText);
|
|
||||||
parent.msgbox("Error loading data: " + error.message, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDockerContainers();
|
|
||||||
|
|
||||||
function addContainerItem(item) {
|
|
||||||
if (lines[item]) {
|
|
||||||
parent.addContainerItem(lines[item]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user