mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 03:30:05 +00:00
Adds dynamic table with initial settings page
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
export * from "./createUser";
|
export * from "./createUser";
|
||||||
export * from "./getToken";
|
export * from "./getToken";
|
||||||
export * from "./getUser";
|
export * from "./getUser";
|
||||||
|
export * from "./models";
|
||||||
export * from "./refreshToken";
|
export * from "./refreshToken";
|
||||||
export * from "./requestHealth";
|
export * from "./requestHealth";
|
||||||
|
export * from "./requestSettings";
|
||||||
export * from "./responseTypes";
|
export * from "./responseTypes";
|
||||||
|
12
frontend/src/api/npm/models.ts
Normal file
12
frontend/src/api/npm/models.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface Sort {
|
||||||
|
field: string;
|
||||||
|
direction: "ASC" | "DESC";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Setting {
|
||||||
|
id: number;
|
||||||
|
createdOn: number;
|
||||||
|
modifiedOn: number;
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
import * as api from "./base";
|
import * as api from "./base";
|
||||||
import { HealthResponse } from "./responseTypes";
|
import { HealthResponse } from "./responseTypes";
|
||||||
|
|
||||||
// Request function.
|
|
||||||
export async function requestHealth(
|
export async function requestHealth(
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
): Promise<HealthResponse> {
|
): Promise<HealthResponse> {
|
||||||
|
16
frontend/src/api/npm/requestSettings.ts
Normal file
16
frontend/src/api/npm/requestSettings.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as api from "./base";
|
||||||
|
import { SettingsResponse } from "./responseTypes";
|
||||||
|
|
||||||
|
export async function requestSettings(
|
||||||
|
offset?: number,
|
||||||
|
abortController?: AbortController,
|
||||||
|
): Promise<SettingsResponse> {
|
||||||
|
const { result } = await api.get(
|
||||||
|
{
|
||||||
|
url: "settings",
|
||||||
|
params: { limit: 20, offset: offset || 0 },
|
||||||
|
},
|
||||||
|
abortController,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { Sort, Setting } from "./models";
|
||||||
|
|
||||||
export interface HealthResponse {
|
export interface HealthResponse {
|
||||||
commit: string;
|
commit: string;
|
||||||
errorReporting: boolean;
|
errorReporting: boolean;
|
||||||
@@ -31,3 +33,11 @@ export interface UserResponse {
|
|||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
auth?: UserAuthResponse;
|
auth?: UserAuthResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SettingsResponse {
|
||||||
|
total: number;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
sort: Sort[];
|
||||||
|
items: Setting[];
|
||||||
|
}
|
||||||
|
239
frontend/src/components/Table/Table.tsx
Normal file
239
frontend/src/components/Table/Table.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import cn from "classnames";
|
||||||
|
|
||||||
|
export interface TableColumn {
|
||||||
|
/**
|
||||||
|
* Column Name, should match the dataset keys
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Column Title
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* Function to perform when rendering this field
|
||||||
|
*/
|
||||||
|
formatter?: any;
|
||||||
|
/**
|
||||||
|
* Additional classes
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TablePagination {
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
total: number;
|
||||||
|
onSetOffset?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableProps {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* Columns
|
||||||
|
*/
|
||||||
|
columns: TableColumn[];
|
||||||
|
/**
|
||||||
|
* data to render
|
||||||
|
*/
|
||||||
|
data: any;
|
||||||
|
/**
|
||||||
|
* Pagination
|
||||||
|
*/
|
||||||
|
pagination?: TablePagination;
|
||||||
|
/**
|
||||||
|
* Name of column to show sorted by
|
||||||
|
*/
|
||||||
|
sortBy?: string;
|
||||||
|
}
|
||||||
|
export const Table = ({
|
||||||
|
title,
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
sortBy,
|
||||||
|
}: TableProps) => {
|
||||||
|
const getFormatter = (given: any) => {
|
||||||
|
if (typeof given === "string") {
|
||||||
|
switch (given) {
|
||||||
|
// Simple ID column has text-muted
|
||||||
|
case "id":
|
||||||
|
return (val: any) => {
|
||||||
|
return <span className="text-muted">{val}</span>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return given;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPagination = (p: TablePagination) => {
|
||||||
|
const totalPages = Math.ceil(p.total / p.limit);
|
||||||
|
const currentPage = Math.floor(p.offset / p.limit) + 1;
|
||||||
|
const end = p.total < p.limit ? p.total : p.offset + p.limit;
|
||||||
|
|
||||||
|
const getPageList = () => {
|
||||||
|
const list = [];
|
||||||
|
for (let x = 0; x < totalPages; x++) {
|
||||||
|
list.push(
|
||||||
|
<li
|
||||||
|
key={`table-pagination-${x}`}
|
||||||
|
className={cn("page-item", { active: currentPage === x + 1 })}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={
|
||||||
|
p.onSetOffset
|
||||||
|
? () => {
|
||||||
|
p.onSetOffset(x * p.limit);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
|
{x + 1}
|
||||||
|
</button>
|
||||||
|
</li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-footer d-flex align-items-center">
|
||||||
|
<p className="m-0 text-muted">
|
||||||
|
Showing <span>{p.offset + 1}</span> to <span>{end}</span> of{" "}
|
||||||
|
<span>{p.total}</span> item{p.total === 1 ? "" : "s"}
|
||||||
|
</p>
|
||||||
|
{end >= p.total ? (
|
||||||
|
<ul className="pagination m-0 ms-auto">
|
||||||
|
<li className={cn("page-item", { disabled: currentPage <= 1 })}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-disabled={currentPage <= 1}
|
||||||
|
onClick={
|
||||||
|
p.onSetOffset
|
||||||
|
? () => {
|
||||||
|
p.onSetOffset(p.offset - p.limit);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="icon"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<polyline points="15 6 9 12 15 18"></polyline>
|
||||||
|
</svg>
|
||||||
|
prev
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{getPageList()}
|
||||||
|
<li
|
||||||
|
className={cn("page-item", {
|
||||||
|
disabled: currentPage >= totalPages,
|
||||||
|
})}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
aria-disabled={currentPage >= totalPages}
|
||||||
|
onClick={
|
||||||
|
p.onSetOffset
|
||||||
|
? () => {
|
||||||
|
p.onSetOffset(p.offset + p.limit);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}>
|
||||||
|
next
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="icon"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
|
<polyline points="9 6 15 12 9 18"></polyline>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{title ? (
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">{title}</h3>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter text-nowrap datatable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{columns.map((col, idx) => {
|
||||||
|
return (
|
||||||
|
<th key={`table-col-${idx}`} className={col.className}>
|
||||||
|
{col.title}
|
||||||
|
{sortBy === col.name ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="icon icon-sm text-dark icon-thick"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round">
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"></path>
|
||||||
|
<polyline points="6 15 12 9 18 15"></polyline>
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((row: any, idx: number) => {
|
||||||
|
return (
|
||||||
|
<tr key={`table-row-${idx}`}>
|
||||||
|
{columns.map((col, idx2) => {
|
||||||
|
return (
|
||||||
|
<td key={`table-col-${idx}-${idx2}`}>
|
||||||
|
{col.formatter
|
||||||
|
? getFormatter(col.formatter)(row[col.name], row)
|
||||||
|
: row[col.name]}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{pagination ? getPagination(pagination) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/Table/index.ts
Normal file
1
frontend/src/components/Table/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./Table";
|
@@ -14,4 +14,5 @@ export * from "./Router";
|
|||||||
export * from "./SinglePage";
|
export * from "./SinglePage";
|
||||||
export * from "./SiteWrapper";
|
export * from "./SiteWrapper";
|
||||||
export * from "./SuspenseLoader";
|
export * from "./SuspenseLoader";
|
||||||
|
export * from "./Table";
|
||||||
export * from "./Unhealthy";
|
export * from "./Unhealthy";
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
import { SettingsResponse, requestSettings } from "api/npm";
|
||||||
|
import { Table } from "components";
|
||||||
|
import { SuspenseLoader } from "components";
|
||||||
|
import { useInterval } from "rooks";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
@@ -8,7 +12,64 @@ const Root = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
return <Root>Settings</Root>;
|
const [data, setData] = useState({} as SettingsResponse);
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
|
||||||
|
const asyncFetch = useCallback(() => {
|
||||||
|
requestSettings(offset)
|
||||||
|
.then(setData)
|
||||||
|
.catch((error: any) => {
|
||||||
|
console.error("fetch data failed", error);
|
||||||
|
});
|
||||||
|
}, [offset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
asyncFetch();
|
||||||
|
}, [asyncFetch]);
|
||||||
|
|
||||||
|
// 1 Minute
|
||||||
|
useInterval(asyncFetch, 1 * 60 * 1000, true);
|
||||||
|
|
||||||
|
const cols = [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
title: "ID",
|
||||||
|
formatter: "id",
|
||||||
|
className: "w-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (typeof data.items !== "undefined") {
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-status-top bg-cyan" />
|
||||||
|
<Table
|
||||||
|
title="Settings"
|
||||||
|
columns={cols}
|
||||||
|
data={data.items}
|
||||||
|
sortBy={data.sort[0].field}
|
||||||
|
pagination={{
|
||||||
|
limit: data.limit,
|
||||||
|
offset: data.offset,
|
||||||
|
total: data.total,
|
||||||
|
onSetOffset: (num: number) => {
|
||||||
|
if (offset !== num) {
|
||||||
|
setOffset(num);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SuspenseLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
Reference in New Issue
Block a user