Skip to content

Commit 5ec02d8

Browse files
committed
Adds dynamic table with initial settings page
1 parent 1bb66c1 commit 5ec02d8

File tree

9 files changed

+344
-3
lines changed

9 files changed

+344
-3
lines changed

frontend/src/api/npm/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export * from "./createUser";
22
export * from "./getToken";
33
export * from "./getUser";
4+
export * from "./models";
45
export * from "./refreshToken";
56
export * from "./requestHealth";
7+
export * from "./requestSettings";
68
export * from "./responseTypes";

frontend/src/api/npm/models.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export interface Sort {
2+
field: string;
3+
direction: "ASC" | "DESC";
4+
}
5+
6+
export interface Setting {
7+
id: number;
8+
createdOn: number;
9+
modifiedOn: number;
10+
name: string;
11+
value: any;
12+
}

frontend/src/api/npm/requestHealth.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as api from "./base";
22
import { HealthResponse } from "./responseTypes";
33

4-
// Request function.
54
export async function requestHealth(
65
abortController?: AbortController,
76
): Promise<HealthResponse> {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as api from "./base";
2+
import { SettingsResponse } from "./responseTypes";
3+
4+
export async function requestSettings(
5+
offset?: number,
6+
abortController?: AbortController,
7+
): Promise<SettingsResponse> {
8+
const { result } = await api.get(
9+
{
10+
url: "settings",
11+
params: { limit: 20, offset: offset || 0 },
12+
},
13+
abortController,
14+
);
15+
return result;
16+
}

frontend/src/api/npm/responseTypes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Sort, Setting } from "./models";
2+
13
export interface HealthResponse {
24
commit: string;
35
errorReporting: boolean;
@@ -31,3 +33,11 @@ export interface UserResponse {
3133
isDisabled: boolean;
3234
auth?: UserAuthResponse;
3335
}
36+
37+
export interface SettingsResponse {
38+
total: number;
39+
offset: number;
40+
limit: number;
41+
sort: Sort[];
42+
items: Setting[];
43+
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import React from "react";
2+
3+
import cn from "classnames";
4+
5+
export interface TableColumn {
6+
/**
7+
* Column Name, should match the dataset keys
8+
*/
9+
name: string;
10+
/**
11+
* Column Title
12+
*/
13+
title: string;
14+
/**
15+
* Function to perform when rendering this field
16+
*/
17+
formatter?: any;
18+
/**
19+
* Additional classes
20+
*/
21+
className?: string;
22+
}
23+
24+
export interface TablePagination {
25+
limit: number;
26+
offset: number;
27+
total: number;
28+
onSetOffset?: any;
29+
}
30+
31+
export interface TableProps {
32+
/**
33+
*
34+
*/
35+
title?: string;
36+
/**
37+
* Columns
38+
*/
39+
columns: TableColumn[];
40+
/**
41+
* data to render
42+
*/
43+
data: any;
44+
/**
45+
* Pagination
46+
*/
47+
pagination?: TablePagination;
48+
/**
49+
* Name of column to show sorted by
50+
*/
51+
sortBy?: string;
52+
}
53+
export const Table = ({
54+
title,
55+
columns,
56+
data,
57+
pagination,
58+
sortBy,
59+
}: TableProps) => {
60+
const getFormatter = (given: any) => {
61+
if (typeof given === "string") {
62+
switch (given) {
63+
// Simple ID column has text-muted
64+
case "id":
65+
return (val: any) => {
66+
return <span className="text-muted">{val}</span>;
67+
};
68+
}
69+
}
70+
71+
return given;
72+
};
73+
74+
const getPagination = (p: TablePagination) => {
75+
const totalPages = Math.ceil(p.total / p.limit);
76+
const currentPage = Math.floor(p.offset / p.limit) + 1;
77+
const end = p.total < p.limit ? p.total : p.offset + p.limit;
78+
79+
const getPageList = () => {
80+
const list = [];
81+
for (let x = 0; x < totalPages; x++) {
82+
list.push(
83+
<li
84+
key={`table-pagination-${x}`}
85+
className={cn("page-item", { active: currentPage === x + 1 })}>
86+
<button
87+
className="page-link"
88+
onClick={
89+
p.onSetOffset
90+
? () => {
91+
p.onSetOffset(x * p.limit);
92+
}
93+
: undefined
94+
}>
95+
{x + 1}
96+
</button>
97+
</li>,
98+
);
99+
}
100+
return list;
101+
};
102+
103+
return (
104+
<div className="card-footer d-flex align-items-center">
105+
<p className="m-0 text-muted">
106+
Showing <span>{p.offset + 1}</span> to <span>{end}</span> of{" "}
107+
<span>{p.total}</span> item{p.total === 1 ? "" : "s"}
108+
</p>
109+
{end >= p.total ? (
110+
<ul className="pagination m-0 ms-auto">
111+
<li className={cn("page-item", { disabled: currentPage <= 1 })}>
112+
<button
113+
className="page-link"
114+
tabIndex={-1}
115+
aria-disabled={currentPage <= 1}
116+
onClick={
117+
p.onSetOffset
118+
? () => {
119+
p.onSetOffset(p.offset - p.limit);
120+
}
121+
: undefined
122+
}>
123+
<svg
124+
xmlns="http://www.w3.org/2000/svg"
125+
className="icon"
126+
width="24"
127+
height="24"
128+
viewBox="0 0 24 24"
129+
strokeWidth="2"
130+
stroke="currentColor"
131+
fill="none"
132+
strokeLinecap="round"
133+
strokeLinejoin="round">
134+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
135+
<polyline points="15 6 9 12 15 18"></polyline>
136+
</svg>
137+
prev
138+
</button>
139+
</li>
140+
{getPageList()}
141+
<li
142+
className={cn("page-item", {
143+
disabled: currentPage >= totalPages,
144+
})}>
145+
<button
146+
className="page-link"
147+
aria-disabled={currentPage >= totalPages}
148+
onClick={
149+
p.onSetOffset
150+
? () => {
151+
p.onSetOffset(p.offset + p.limit);
152+
}
153+
: undefined
154+
}>
155+
next
156+
<svg
157+
xmlns="http://www.w3.org/2000/svg"
158+
className="icon"
159+
width="24"
160+
height="24"
161+
viewBox="0 0 24 24"
162+
strokeWidth="2"
163+
stroke="currentColor"
164+
fill="none"
165+
strokeLinecap="round"
166+
strokeLinejoin="round">
167+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
168+
<polyline points="9 6 15 12 9 18"></polyline>
169+
</svg>
170+
</button>
171+
</li>
172+
</ul>
173+
) : null}
174+
</div>
175+
);
176+
};
177+
178+
return (
179+
<>
180+
{title ? (
181+
<div className="card-header">
182+
<h3 className="card-title">{title}</h3>
183+
</div>
184+
) : null}
185+
<div className="table-responsive">
186+
<table className="table card-table table-vcenter text-nowrap datatable">
187+
<thead>
188+
<tr>
189+
{columns.map((col, idx) => {
190+
return (
191+
<th key={`table-col-${idx}`} className={col.className}>
192+
{col.title}
193+
{sortBy === col.name ? (
194+
<svg
195+
xmlns="http://www.w3.org/2000/svg"
196+
className="icon icon-sm text-dark icon-thick"
197+
width="24"
198+
height="24"
199+
viewBox="0 0 24 24"
200+
strokeWidth="2"
201+
stroke="currentColor"
202+
fill="none"
203+
strokeLinecap="round"
204+
strokeLinejoin="round">
205+
<path
206+
stroke="none"
207+
d="M0 0h24v24H0z"
208+
fill="none"></path>
209+
<polyline points="6 15 12 9 18 15"></polyline>
210+
</svg>
211+
) : null}
212+
</th>
213+
);
214+
})}
215+
</tr>
216+
</thead>
217+
<tbody>
218+
{data.map((row: any, idx: number) => {
219+
return (
220+
<tr key={`table-row-${idx}`}>
221+
{columns.map((col, idx2) => {
222+
return (
223+
<td key={`table-col-${idx}-${idx2}`}>
224+
{col.formatter
225+
? getFormatter(col.formatter)(row[col.name], row)
226+
: row[col.name]}
227+
</td>
228+
);
229+
})}
230+
</tr>
231+
);
232+
})}
233+
</tbody>
234+
</table>
235+
</div>
236+
{pagination ? getPagination(pagination) : null}
237+
</>
238+
);
239+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./Table";

frontend/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export * from "./Router";
1414
export * from "./SinglePage";
1515
export * from "./SiteWrapper";
1616
export * from "./SuspenseLoader";
17+
export * from "./Table";
1718
export * from "./Unhealthy";

frontend/src/pages/Settings/index.tsx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import React from "react";
1+
import React, { useState, useEffect, useCallback } from "react";
22

3+
import { SettingsResponse, requestSettings } from "api/npm";
4+
import { Table } from "components";
5+
import { SuspenseLoader } from "components";
6+
import { useInterval } from "rooks";
37
import styled from "styled-components";
48

59
const Root = styled.div`
@@ -8,7 +12,64 @@ const Root = styled.div`
812
`;
913

1014
function Settings() {
11-
return <Root>Settings</Root>;
15+
const [data, setData] = useState({} as SettingsResponse);
16+
const [offset, setOffset] = useState(0);
17+
18+
const asyncFetch = useCallback(() => {
19+
requestSettings(offset)
20+
.then(setData)
21+
.catch((error: any) => {
22+
console.error("fetch data failed", error);
23+
});
24+
}, [offset]);
25+
26+
useEffect(() => {
27+
asyncFetch();
28+
}, [asyncFetch]);
29+
30+
// 1 Minute
31+
useInterval(asyncFetch, 1 * 60 * 1000, true);
32+
33+
const cols = [
34+
{
35+
name: "id",
36+
title: "ID",
37+
formatter: "id",
38+
className: "w-1",
39+
},
40+
{
41+
name: "name",
42+
title: "Name",
43+
},
44+
];
45+
46+
if (typeof data.items !== "undefined") {
47+
return (
48+
<Root>
49+
<div className="card">
50+
<div className="card-status-top bg-cyan" />
51+
<Table
52+
title="Settings"
53+
columns={cols}
54+
data={data.items}
55+
sortBy={data.sort[0].field}
56+
pagination={{
57+
limit: data.limit,
58+
offset: data.offset,
59+
total: data.total,
60+
onSetOffset: (num: number) => {
61+
if (offset !== num) {
62+
setOffset(num);
63+
}
64+
},
65+
}}
66+
/>
67+
</div>
68+
</Root>
69+
);
70+
}
71+
72+
return <SuspenseLoader />;
1273
}
1374

1475
export default Settings;

0 commit comments

Comments
 (0)