Adds dynamic table with initial settings page

This commit is contained in:
Jamie Curnow
2021-07-26 00:14:00 +10:00
parent 1bb66c13d5
commit 5ec02d8cb0
9 changed files with 344 additions and 3 deletions

View File

@@ -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";

View 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;
}

View File

@@ -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> {

View 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;
}

View File

@@ -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[];
}

View 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}
</>
);
};

View File

@@ -0,0 +1 @@
export * from "./Table";

View File

@@ -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";

View File

@@ -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;