mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-10-01 01:09:51 +01:00
Merge pull request #5772 from drakhaw/develop
feat: per-path access lists, host logs modal
This commit is contained in:
@@ -2,7 +2,9 @@ import fs from "node:fs";
|
||||
import batchflow from "batchflow";
|
||||
import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import { isMysql, isPostgres } from "../lib/config.js";
|
||||
import utils from "../lib/utils.js";
|
||||
import db from "../db.js";
|
||||
import { access as logger } from "../logger.js";
|
||||
import accessListModel from "../models/access_list.js";
|
||||
import accessListAuthModel from "../models/access_list_auth.js";
|
||||
@@ -15,6 +17,36 @@ const omissions = () => {
|
||||
return ["is_deleted"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find proxy hosts that reference an access list in their locations JSON.
|
||||
*
|
||||
* @param {Integer} accessListId
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
const getProxyHostsUsingAccessListInLocations = async (accessListId) => {
|
||||
let result;
|
||||
if (isMysql()) {
|
||||
const searchObj = JSON.stringify([{ access_list_id: accessListId }]);
|
||||
result = await db().raw(
|
||||
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND JSON_CONTAINS(locations, ?, ?)",
|
||||
[searchObj, "$"],
|
||||
);
|
||||
} else if (isPostgres()) {
|
||||
result = await db().raw(
|
||||
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations::jsonb @> ?::jsonb",
|
||||
[JSON.stringify([{ access_list_id: accessListId }])],
|
||||
);
|
||||
} else {
|
||||
result = await db().raw(
|
||||
"SELECT id FROM proxy_host WHERE is_deleted = 0 AND locations LIKE ?",
|
||||
[`%"access_list_id":${accessListId}%`],
|
||||
);
|
||||
}
|
||||
// knex raw() returns [rows, metadata] for MySQL
|
||||
const rows = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result;
|
||||
return rows || [];
|
||||
};
|
||||
|
||||
const internalAccessList = {
|
||||
/**
|
||||
* @param {Access} access
|
||||
@@ -187,6 +219,44 @@ const internalAccessList = {
|
||||
if (Number.parseInt(freshRow.proxy_host_count, 10)) {
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", freshRow.proxy_hosts);
|
||||
}
|
||||
|
||||
// Also regenerate configs for proxy hosts that reference this access list in their locations
|
||||
const locationHostRows = await getProxyHostsUsingAccessListInLocations(data.id);
|
||||
if (locationHostRows?.length) {
|
||||
const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => {
|
||||
// Exclude hosts already regenerated above
|
||||
return !freshRow.proxy_hosts?.find((h) => h.id === id);
|
||||
});
|
||||
if (locationHostIds.length) {
|
||||
const locationHosts = await proxyHostModel.query()
|
||||
.where("is_deleted", 0)
|
||||
.whereIn("id", locationHostIds)
|
||||
.allowGraph(proxyHostModel.defaultAllowGraph)
|
||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
||||
for (const host of locationHosts) {
|
||||
// Fetch access lists for locations
|
||||
if (host.locations?.length) {
|
||||
for (let i = 0; i < host.locations.length; i++) {
|
||||
const loc = host.locations[i];
|
||||
if (loc.access_list_id && loc.access_list_id > 0) {
|
||||
const locAccessList = await accessListModel
|
||||
.query()
|
||||
.allowGraph("[clients,items]")
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("id", loc.access_list_id)
|
||||
.withGraphFetched("[clients,items]")
|
||||
.first();
|
||||
if (locAccessList) {
|
||||
host.locations[i].access_list = locAccessList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
|
||||
}
|
||||
}
|
||||
|
||||
await internalNginx.reload();
|
||||
return internalAccessList.maskItems(freshRow);
|
||||
},
|
||||
@@ -281,6 +351,37 @@ const internalAccessList = {
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", row.proxy_hosts);
|
||||
}
|
||||
|
||||
// Also handle proxy hosts that reference this access list in their locations JSON
|
||||
const locationHostRows = await getProxyHostsUsingAccessListInLocations(row.id);
|
||||
if (locationHostRows?.length) {
|
||||
const locationHostIds = locationHostRows.map((r) => r.id).filter((id) => {
|
||||
return !row.proxy_hosts?.find((h) => h.id === id);
|
||||
});
|
||||
if (locationHostIds.length) {
|
||||
// Clear the access_list_id in locations JSON for these hosts
|
||||
for (const hostId of locationHostIds) {
|
||||
const host = await proxyHostModel.query().where("id", hostId).first();
|
||||
if (host?.locations) {
|
||||
const updatedLocations = host.locations.map((loc) => {
|
||||
if (loc.access_list_id === row.id) {
|
||||
return { ...loc, access_list_id: 0 };
|
||||
}
|
||||
return loc;
|
||||
});
|
||||
await proxyHostModel.query().where("id", hostId).patch({ locations: updatedLocations });
|
||||
}
|
||||
}
|
||||
|
||||
// Re-fetch and regenerate configs
|
||||
const locationHosts = await proxyHostModel.query()
|
||||
.where("is_deleted", 0)
|
||||
.whereIn("id", locationHostIds)
|
||||
.allowGraph(proxyHostModel.defaultExpand)
|
||||
.withGraphFetched("[owner, certificate, access_list.[clients,items]]");
|
||||
await internalNginx.bulkGenerateConfigs("proxy_host", locationHosts);
|
||||
}
|
||||
}
|
||||
|
||||
await internalNginx.reload();
|
||||
|
||||
// delete the htpasswd file
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from "lodash";
|
||||
import errs from "../lib/error.js";
|
||||
import { castJsonIfNeed } from "../lib/helpers.js";
|
||||
import utils from "../lib/utils.js";
|
||||
import accessListModel from "../models/access_list.js";
|
||||
import proxyHostModel from "../models/proxy_host.js";
|
||||
import internalAuditLog from "./audit-log.js";
|
||||
import internalCertificate from "./certificate.js";
|
||||
@@ -12,6 +13,33 @@ const omissions = () => {
|
||||
return ["is_deleted", "owner.is_deleted"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches access lists for each location that has its own access_list_id.
|
||||
* Attaches the expanded access_list object (with clients and items) to each location.
|
||||
*
|
||||
* @param {Object} host
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const fetchLocationAccessLists = async (host) => {
|
||||
if (!host.locations?.length) {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < host.locations.length; i++) {
|
||||
const loc = host.locations[i];
|
||||
if (loc.access_list_id && loc.access_list_id > 0) {
|
||||
const accessList = await accessListModel
|
||||
.query()
|
||||
.where("is_deleted", 0)
|
||||
.andWhere("id", loc.access_list_id)
|
||||
.withGraphFetched("[clients,items]")
|
||||
.first();
|
||||
if (accessList) {
|
||||
host.locations[i].access_list = accessList;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const internalProxyHost = {
|
||||
/**
|
||||
* @param {Access} access
|
||||
@@ -83,28 +111,29 @@ const internalProxyHost = {
|
||||
expand: ["certificate", "owner", "access_list.[clients,items]"],
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
|
||||
.then(async (row) => {
|
||||
await fetchLocationAccessLists(row);
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => {
|
||||
return row;
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
// Audit log
|
||||
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
|
||||
|
||||
// Add to audit log
|
||||
return internalAuditLog
|
||||
.add(access, {
|
||||
action: "created",
|
||||
object_type: "proxy-host",
|
||||
object_id: row.id,
|
||||
meta: thisData,
|
||||
})
|
||||
.then(() => {
|
||||
return row;
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
// Audit log
|
||||
thisData.meta = _.assign({}, thisData.meta || {}, row.meta);
|
||||
|
||||
// Add to audit log
|
||||
return internalAuditLog
|
||||
.add(access, {
|
||||
action: "created",
|
||||
object_type: "proxy-host",
|
||||
object_id: row.id,
|
||||
meta: thisData,
|
||||
})
|
||||
.then(() => {
|
||||
return row;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -202,24 +231,25 @@ const internalProxyHost = {
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return internalProxyHost
|
||||
.get(access, {
|
||||
id: thisData.id,
|
||||
expand: ["owner", "certificate", "access_list.[clients,items]"],
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row.enabled) {
|
||||
// No need to add nginx config if host is disabled
|
||||
return row;
|
||||
}
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
|
||||
row.meta = new_meta;
|
||||
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
|
||||
});
|
||||
.then(() => {
|
||||
return internalProxyHost
|
||||
.get(access, {
|
||||
id: thisData.id,
|
||||
expand: ["owner", "certificate", "access_list.[clients,items]"],
|
||||
})
|
||||
.then(async (row) => {
|
||||
if (!row.enabled) {
|
||||
// No need to add nginx config if host is disabled
|
||||
return row;
|
||||
}
|
||||
await fetchLocationAccessLists(row);
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => {
|
||||
row.meta = new_meta;
|
||||
return _.omit(internalHost.cleanRowCertificateMeta(row), omissions());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -326,39 +356,38 @@ const internalProxyHost = {
|
||||
expand: ["certificate", "owner", "access_list"],
|
||||
});
|
||||
})
|
||||
.then((row) => {
|
||||
if (!row?.id) {
|
||||
throw new errs.ItemNotFoundError(data.id);
|
||||
}
|
||||
if (row.enabled) {
|
||||
throw new errs.ValidationError("Host is already enabled");
|
||||
}
|
||||
.then(async (row) => {
|
||||
if (!row?.id) {
|
||||
throw new errs.ItemNotFoundError(data.id);
|
||||
}
|
||||
if (row.enabled) {
|
||||
throw new errs.ValidationError("Host is already enabled");
|
||||
}
|
||||
|
||||
row.enabled = 1;
|
||||
row.enabled = 1;
|
||||
|
||||
return proxyHostModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
enabled: 1,
|
||||
})
|
||||
.then(() => {
|
||||
// Configure nginx
|
||||
return internalNginx.configure(proxyHostModel, "proxy_host", row);
|
||||
})
|
||||
.then(() => {
|
||||
// Add to audit log
|
||||
return internalAuditLog.add(access, {
|
||||
action: "enabled",
|
||||
object_type: "proxy-host",
|
||||
object_id: row.id,
|
||||
meta: _.omit(row, omissions()),
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
return true;
|
||||
await proxyHostModel
|
||||
.query()
|
||||
.where("id", row.id)
|
||||
.patch({
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
await fetchLocationAccessLists(row);
|
||||
|
||||
// Configure nginx
|
||||
await internalNginx.configure(proxyHostModel, "proxy_host", row);
|
||||
|
||||
// Add to audit log
|
||||
await internalAuditLog.add(access, {
|
||||
action: "enabled",
|
||||
object_type: "proxy-host",
|
||||
object_id: row.id,
|
||||
meta: _.omit(row, omissions()),
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import express from "express";
|
||||
import fs from "node:fs";
|
||||
import internalProxyHost from "../../internal/proxy-host.js";
|
||||
import jwtdecode from "../../lib/express/jwt-decode.js";
|
||||
import apiValidator from "../../lib/validator/api.js";
|
||||
@@ -206,4 +207,70 @@ router
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy-host logs
|
||||
*
|
||||
* /api/nginx/proxy-hosts/123/logs
|
||||
*/
|
||||
router
|
||||
.route("/:host_id/logs")
|
||||
.options((_, res) => {
|
||||
res.sendStatus(204);
|
||||
})
|
||||
.all(jwtdecode())
|
||||
|
||||
/**
|
||||
* GET /api/nginx/proxy-hosts/123/logs
|
||||
*
|
||||
* Retrieve logs for a specific proxy-host
|
||||
*/
|
||||
.get(async (req, res, next) => {
|
||||
try {
|
||||
const data = await validator(
|
||||
{
|
||||
required: ["host_id"],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
host_id: {
|
||||
$ref: "common#/properties/id",
|
||||
},
|
||||
type: {
|
||||
type: "string",
|
||||
enum: ["access", "error"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
host_id: req.params.host_id,
|
||||
type: req.query.type || "access",
|
||||
},
|
||||
);
|
||||
|
||||
const hostId = Number.parseInt(data.host_id, 10);
|
||||
const logType = data.type === "error" ? "error" : "access";
|
||||
const logFile = `/data/logs/proxy-host-${hostId}_${logType}.log`;
|
||||
|
||||
// Check access permission
|
||||
await res.locals.access.can("proxy_hosts:get", hostId);
|
||||
|
||||
let logs = "";
|
||||
if (fs.existsSync(logFile)) {
|
||||
const content = fs.readFileSync(logFile, { encoding: "utf8" });
|
||||
const lines = content.split("\n");
|
||||
// Return last 1000 lines to avoid huge payloads
|
||||
const maxLines = 1000;
|
||||
if (lines.length > maxLines) {
|
||||
logs = lines.slice(-maxLines).join("\n");
|
||||
} else {
|
||||
logs = content;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send({ logs });
|
||||
} catch (err) {
|
||||
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
},
|
||||
"advanced_config": {
|
||||
"type": "string"
|
||||
},
|
||||
"access_list_id": {
|
||||
"$ref": "../common.json#/properties/access_list_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -132,7 +135,8 @@
|
||||
"path": "/app",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "example.com",
|
||||
"forward_port": 80
|
||||
"forward_port": 80,
|
||||
"access_list_id": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import certificateModel from "./models/certificate.js";
|
||||
import settingModel from "./models/setting.js";
|
||||
import userModel from "./models/user.js";
|
||||
import userPermissionModel from "./models/user_permission.js";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
export const isSetup = async () => {
|
||||
const row = await userModel.query().select("id").where("is_deleted", 0).first();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as api from "./base";
|
||||
|
||||
export async function getProxyHostLogs(id: number, type: "access" | "error" = "access"): Promise<{ logs: string }> {
|
||||
return await api.get({
|
||||
url: `/nginx/proxy-hosts/${id}/logs`,
|
||||
params: { type },
|
||||
});
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export * from "./getHostsReport";
|
||||
export * from "./getLogSources";
|
||||
export * from "./getLogTail";
|
||||
export * from "./getProxyHost";
|
||||
export * from "./getProxyHostLogs";
|
||||
export * from "./getProxyHosts";
|
||||
export * from "./getRedirectionHost";
|
||||
export * from "./getRedirectionHosts";
|
||||
|
||||
@@ -127,6 +127,8 @@ export interface ProxyLocation {
|
||||
forwardScheme: string;
|
||||
forwardHost: string;
|
||||
forwardPort: number;
|
||||
accessListId?: number;
|
||||
accessList?: AccessList;
|
||||
}
|
||||
|
||||
export interface ProxyHost {
|
||||
|
||||
@@ -31,14 +31,18 @@ interface Props {
|
||||
id?: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
onFormChange?: (value: number) => void;
|
||||
}
|
||||
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId" }: Props) {
|
||||
export function AccessField({ name = "accessListId", label = "access-list", id = "accessListId", onFormChange }: Props) {
|
||||
const { locale } = useLocaleState();
|
||||
const { isLoading, isError, error, data } = useAccessLists(["owner", "items", "clients"]);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
const handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {
|
||||
setFieldValue(name, newValue?.value);
|
||||
if (onFormChange) {
|
||||
onFormChange(newValue?.value ?? 0);
|
||||
}
|
||||
};
|
||||
|
||||
const options: AccessOption[] =
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useFormikContext } from "formik";
|
||||
import { useRef, useState } from "react";
|
||||
import type { ProxyLocation } from "src/api/backend";
|
||||
import { intl, T } from "src/locale";
|
||||
import { AccessField } from "./AccessField";
|
||||
import styles from "./LocationsFields.module.css";
|
||||
|
||||
// Below this many locations the list is short enough to scan by eye, and the
|
||||
@@ -45,6 +46,7 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
forwardScheme: "http",
|
||||
forwardHost: "",
|
||||
forwardPort: 80,
|
||||
accessListId: 0,
|
||||
};
|
||||
|
||||
const toggleExpanded = (id: number) => {
|
||||
@@ -79,6 +81,12 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
setFormField(newRows);
|
||||
};
|
||||
|
||||
const handleAccessListChange = (id: number, accessListId: number) => {
|
||||
const newRows = rows.map((r: Row) => (r.id === id ? { ...r, value: { ...r.value, accessListId } } : r));
|
||||
setRows(newRows);
|
||||
setFormField(newRows);
|
||||
};
|
||||
|
||||
const setFormField = (newRows: Row[]) => {
|
||||
const filtered = newRows.map((r: Row) => r.value).filter((v: ProxyLocation) => v?.path?.trim() !== "");
|
||||
setFieldValue(name, filtered);
|
||||
@@ -299,6 +307,12 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessField
|
||||
name={`locations[${row.id}].accessListId`}
|
||||
label="access-list"
|
||||
id={`locations-access-list-${row.id}`}
|
||||
onFormChange={(value) => handleAccessListChange(row.id, value)}
|
||||
/>
|
||||
{advVisible.includes(row.id) && (
|
||||
<div className="">
|
||||
<CodeEditor
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from "./useHostReport";
|
||||
export * from "./useLogSources";
|
||||
export * from "./useLogTail";
|
||||
export * from "./useProxyHost";
|
||||
export * from "./useProxyHostLogs";
|
||||
export * from "./useProxyHosts";
|
||||
export * from "./useRedirectionHost";
|
||||
export * from "./useRedirectionHosts";
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getProxyHostLogs } from "src/api/backend";
|
||||
|
||||
const useProxyHostLogs = (id: number, type: "access" | "error" = "access") => {
|
||||
return useQuery<{ logs: string }, Error>({
|
||||
queryKey: ["proxy-host-logs", id, type],
|
||||
queryFn: () => getProxyHostLogs(id, type),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
};
|
||||
|
||||
export { useProxyHostLogs };
|
||||
@@ -125,6 +125,9 @@
|
||||
"action.enable": {
|
||||
"defaultMessage": "Enable"
|
||||
},
|
||||
"action.logs": {
|
||||
"defaultMessage": "Logs"
|
||||
},
|
||||
"action.permissions": {
|
||||
"defaultMessage": "Permissions"
|
||||
},
|
||||
@@ -269,6 +272,9 @@
|
||||
"column.email": {
|
||||
"defaultMessage": "Email"
|
||||
},
|
||||
"column.error": {
|
||||
"defaultMessage": "Error"
|
||||
},
|
||||
"column.event": {
|
||||
"defaultMessage": "Event"
|
||||
},
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"action.enable": {
|
||||
"defaultMessage": "Lülita sisse"
|
||||
},
|
||||
"action.logs": {
|
||||
"defaultMessage": "Logid"
|
||||
},
|
||||
"action.permissions": {
|
||||
"defaultMessage": "Õigused"
|
||||
},
|
||||
@@ -269,6 +272,9 @@
|
||||
"column.email": {
|
||||
"defaultMessage": "E-post"
|
||||
},
|
||||
"column.error": {
|
||||
"defaultMessage": "Viga"
|
||||
},
|
||||
"column.event": {
|
||||
"defaultMessage": "Sündmus"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import CodeEditor from "@uiw/react-textarea-code-editor";
|
||||
import EasyModal, { type InnerModalProps } from "ez-modal-react";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-bootstrap";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import { Button, Loading } from "src/components";
|
||||
import { useProxyHostLogs } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
|
||||
const showHostLogsModal = (id: number) => {
|
||||
EasyModal.show(HostLogsModal, { id });
|
||||
};
|
||||
|
||||
interface Props extends InnerModalProps {
|
||||
id: number;
|
||||
}
|
||||
const HostLogsModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
const [logType, setLogType] = useState<"access" | "error">("access");
|
||||
const { data, isLoading, error } = useProxyHostLogs(id, logType);
|
||||
|
||||
return (
|
||||
<Modal show={visible} onHide={remove} size="lg" scrollable>
|
||||
{!isLoading && error && (
|
||||
<Alert variant="danger" className="m-3">
|
||||
{error?.message || "Unknown error"}
|
||||
</Alert>
|
||||
)}
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<T id="action.logs" />
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="mb-3">
|
||||
<ul className="nav nav-tabs">
|
||||
<li className="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
className={`nav-link ${logType === "access" ? "active" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setLogType("access");
|
||||
}}
|
||||
>
|
||||
<T id="column.access" />
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
className={`nav-link ${logType === "error" ? "active" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setLogType("error");
|
||||
}}
|
||||
>
|
||||
<T id="column.error" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loading noLogo />
|
||||
) : (
|
||||
<div style={{ maxHeight: "60vh", overflow: "auto" }}>
|
||||
<CodeEditor
|
||||
language="text"
|
||||
padding={15}
|
||||
data-color-mode="dark"
|
||||
indentWidth={2}
|
||||
style={{
|
||||
fontFamily:
|
||||
"ui-monospace,SFMono-Regular,SF Mono,Consolas,Liberation Mono,Menlo,monospace",
|
||||
borderRadius: "0.3rem",
|
||||
backgroundColor: "var(--tblr-bg-surface-dark)",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
readOnly
|
||||
value={data?.logs || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button data-bs-dismiss="modal" onClick={remove}>
|
||||
<T id="action.close" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export { showHostLogsModal };
|
||||
@@ -6,6 +6,7 @@ export * from "./DeleteConfirmModal";
|
||||
export * from "./DNSCertificateModal";
|
||||
export * from "./EventDetailsModal";
|
||||
export * from "./HelpModal";
|
||||
export * from "./HostLogsModal";
|
||||
export * from "./HTTPCertificateModal";
|
||||
export * from "./PermissionsModal";
|
||||
export * from "./ProxyHostModal";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconDotsVertical, IconEdit, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import { IconDotsVertical, IconEdit, IconFileText, IconPower, IconTrash } from "@tabler/icons-react";
|
||||
import { createColumnHelper, type SortingState, useTable } from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ProxyHost } from "src/api/backend";
|
||||
@@ -23,9 +23,19 @@ interface Props {
|
||||
onEdit?: (id: number) => void;
|
||||
onDelete?: (id: number) => void;
|
||||
onDisableToggle?: (id: number, enabled: boolean) => void;
|
||||
onLogs?: (id: number) => void;
|
||||
onNew?: () => void;
|
||||
}
|
||||
export default function Table({ data, isFetching, onEdit, onDelete, onDisableToggle, onNew, isFiltered }: Props) {
|
||||
export default function Table({
|
||||
data,
|
||||
isFetching,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onDisableToggle,
|
||||
onLogs,
|
||||
onNew,
|
||||
isFiltered,
|
||||
}: Props) {
|
||||
const columnHelper = createColumnHelper<Features, ProxyHost>();
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
@@ -122,6 +132,17 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onLogs?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconFileText size={16} />
|
||||
<T id="action.logs" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
@@ -155,7 +176,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
},
|
||||
}),
|
||||
],
|
||||
[columnHelper, onEdit, onDisableToggle, onDelete],
|
||||
[columnHelper, onEdit, onDisableToggle, onDelete, onLogs],
|
||||
);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useProxyHosts } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals";
|
||||
import { showDeleteConfirmModal, showHelpModal, showHostLogsModal, showProxyHostModal } from "src/modals";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
@@ -99,6 +99,7 @@ export default function TableWrapper() {
|
||||
isFiltered={!!search}
|
||||
isFetching={isFetching}
|
||||
onEdit={(id: number) => showProxyHostModal(id)}
|
||||
onLogs={(id: number) => showHostLogsModal(id)}
|
||||
onDelete={(id: number) => {
|
||||
const host = data?.find((h) => h.id === id);
|
||||
showDeleteConfirmModal({
|
||||
|
||||
Reference in New Issue
Block a user