From 5ec02d8cb08b2dec43130f6e9623e99a12a73e36 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 26 Jul 2021 00:14:00 +1000 Subject: [PATCH] Adds dynamic table with initial settings page --- frontend/src/api/npm/index.ts | 2 + frontend/src/api/npm/models.ts | 12 ++ frontend/src/api/npm/requestHealth.ts | 1 - frontend/src/api/npm/requestSettings.ts | 16 ++ frontend/src/api/npm/responseTypes.ts | 10 + frontend/src/components/Table/Table.tsx | 239 ++++++++++++++++++++++++ frontend/src/components/Table/index.ts | 1 + frontend/src/components/index.ts | 1 + frontend/src/pages/Settings/index.tsx | 65 ++++++- 9 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 frontend/src/api/npm/models.ts create mode 100644 frontend/src/api/npm/requestSettings.ts create mode 100644 frontend/src/components/Table/Table.tsx create mode 100644 frontend/src/components/Table/index.ts diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts index c4ffe25c..19da117f 100644 --- a/frontend/src/api/npm/index.ts +++ b/frontend/src/api/npm/index.ts @@ -1,6 +1,8 @@ export * from "./createUser"; export * from "./getToken"; export * from "./getUser"; +export * from "./models"; export * from "./refreshToken"; export * from "./requestHealth"; +export * from "./requestSettings"; export * from "./responseTypes"; diff --git a/frontend/src/api/npm/models.ts b/frontend/src/api/npm/models.ts new file mode 100644 index 00000000..59a3d5a1 --- /dev/null +++ b/frontend/src/api/npm/models.ts @@ -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; +} diff --git a/frontend/src/api/npm/requestHealth.ts b/frontend/src/api/npm/requestHealth.ts index 92a04006..11ec1b9e 100644 --- a/frontend/src/api/npm/requestHealth.ts +++ b/frontend/src/api/npm/requestHealth.ts @@ -1,7 +1,6 @@ import * as api from "./base"; import { HealthResponse } from "./responseTypes"; -// Request function. export async function requestHealth( abortController?: AbortController, ): Promise { diff --git a/frontend/src/api/npm/requestSettings.ts b/frontend/src/api/npm/requestSettings.ts new file mode 100644 index 00000000..1c1e6f71 --- /dev/null +++ b/frontend/src/api/npm/requestSettings.ts @@ -0,0 +1,16 @@ +import * as api from "./base"; +import { SettingsResponse } from "./responseTypes"; + +export async function requestSettings( + offset?: number, + abortController?: AbortController, +): Promise { + const { result } = await api.get( + { + url: "settings", + params: { limit: 20, offset: offset || 0 }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts index 900af8ff..cf0c19f1 100644 --- a/frontend/src/api/npm/responseTypes.ts +++ b/frontend/src/api/npm/responseTypes.ts @@ -1,3 +1,5 @@ +import { Sort, Setting } from "./models"; + export interface HealthResponse { commit: string; errorReporting: boolean; @@ -31,3 +33,11 @@ export interface UserResponse { isDisabled: boolean; auth?: UserAuthResponse; } + +export interface SettingsResponse { + total: number; + offset: number; + limit: number; + sort: Sort[]; + items: Setting[]; +} diff --git a/frontend/src/components/Table/Table.tsx b/frontend/src/components/Table/Table.tsx new file mode 100644 index 00000000..ebd88d96 --- /dev/null +++ b/frontend/src/components/Table/Table.tsx @@ -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 {val}; + }; + } + } + + 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( +
  • + +
  • , + ); + } + return list; + }; + + return ( +
    +

    + Showing {p.offset + 1} to {end} of{" "} + {p.total} item{p.total === 1 ? "" : "s"} +

    + {end >= p.total ? ( +
      +
    • + +
    • + {getPageList()} +
    • = totalPages, + })}> + +
    • +
    + ) : null} +
    + ); + }; + + return ( + <> + {title ? ( +
    +

    {title}

    +
    + ) : null} +
    + + + + {columns.map((col, idx) => { + return ( + + ); + })} + + + + {data.map((row: any, idx: number) => { + return ( + + {columns.map((col, idx2) => { + return ( + + ); + })} + + ); + })} + +
    + {col.title} + {sortBy === col.name ? ( + + + + + ) : null} +
    + {col.formatter + ? getFormatter(col.formatter)(row[col.name], row) + : row[col.name]} +
    +
    + {pagination ? getPagination(pagination) : null} + + ); +}; diff --git a/frontend/src/components/Table/index.ts b/frontend/src/components/Table/index.ts new file mode 100644 index 00000000..e40efa47 --- /dev/null +++ b/frontend/src/components/Table/index.ts @@ -0,0 +1 @@ +export * from "./Table"; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 36bf099e..0aae858f 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -14,4 +14,5 @@ export * from "./Router"; export * from "./SinglePage"; export * from "./SiteWrapper"; export * from "./SuspenseLoader"; +export * from "./Table"; export * from "./Unhealthy"; diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx index 7d5156a2..14aaeaa1 100644 --- a/frontend/src/pages/Settings/index.tsx +++ b/frontend/src/pages/Settings/index.tsx @@ -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"; const Root = styled.div` @@ -8,7 +12,64 @@ const Root = styled.div` `; function Settings() { - return Settings; + 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 ( + +
    +
    + { + if (offset !== num) { + setOffset(num); + } + }, + }} + /> + + + ); + } + + return ; } export default Settings;