mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 11:40:04 +00:00
Adds dynamic table with initial settings page
This commit is contained in:
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}
|
||||
</>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user