From 560f3d9b29de3779ab069ac78bfffea7dab75db9 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 10 Jan 2023 11:46:40 +1000 Subject: [PATCH] Basis for Upstreams UI --- frontend/src/Router.tsx | 2 + frontend/src/api/npm/getUpstreams.ts | 19 +++ frontend/src/api/npm/index.ts | 1 + frontend/src/api/npm/models.ts | 25 +++ frontend/src/api/npm/responseTypes.ts | 5 + frontend/src/components/Table/Formatters.tsx | 25 +++ frontend/src/hooks/index.ts | 1 + frontend/src/hooks/useUpstreams.ts | 41 +++++ frontend/src/locale/src/de.json | 12 ++ frontend/src/locale/src/en.json | 12 ++ frontend/src/locale/src/fa.json | 12 ++ .../src/pages/Upstreams/UpstreamsTable.tsx | 156 ++++++++++++++++++ frontend/src/pages/Upstreams/index.tsx | 100 +++++++++++ 13 files changed, 411 insertions(+) create mode 100644 frontend/src/api/npm/getUpstreams.ts create mode 100644 frontend/src/hooks/useUpstreams.ts create mode 100644 frontend/src/pages/Upstreams/UpstreamsTable.tsx create mode 100644 frontend/src/pages/Upstreams/index.tsx diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index d4796e5..262d1f5 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -18,6 +18,7 @@ const NginxTemplates = lazy(() => import("pages/NginxTemplates")); const Login = lazy(() => import("pages/Login")); const GeneralSettings = lazy(() => import("pages/Settings")); const Setup = lazy(() => import("pages/Setup")); +const Upstreams = lazy(() => import("pages/Upstreams")); const Users = lazy(() => import("pages/Users")); function Router() { @@ -56,6 +57,7 @@ function Router() { } /> + } /> } /> { + const { result } = await api.get( + { + url: "upstreams", + params: { limit, offset, sort, expand: "user", ...filters }, + }, + abortController, + ); + return result; +} diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts index d03fa7a..029fdb1 100644 --- a/frontend/src/api/npm/index.ts +++ b/frontend/src/api/npm/index.ts @@ -12,6 +12,7 @@ export * from "./getHosts"; export * from "./getNginxTemplates"; export * from "./getSettings"; export * from "./getToken"; +export * from "./getUpstreams"; export * from "./getUser"; export * from "./getUsers"; export * from "./helpers"; diff --git a/frontend/src/api/npm/models.ts b/frontend/src/api/npm/models.ts index 4f043a1..2026791 100644 --- a/frontend/src/api/npm/models.ts +++ b/frontend/src/api/npm/models.ts @@ -120,3 +120,28 @@ export interface NginxTemplate { type: string; template: string; } + +export interface Upstream { + // todo + id: number; + createdOn: number; + modifiedOn: number; + userId: number; + type: string; + nginxTemplateId: number; + listenInterface: number; + domainNames: string[]; + upstreamId: number; + certificateId: number; + accessListId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + paths: string; + advancedConfig: string; + isDisabled: boolean; +} diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts index 7d66204..b4b27dd 100644 --- a/frontend/src/api/npm/responseTypes.ts +++ b/frontend/src/api/npm/responseTypes.ts @@ -7,6 +7,7 @@ import { Setting, Sort, User, + Upstream, } from "./models"; export interface BaseResponse { @@ -56,3 +57,7 @@ export interface HostsResponse extends BaseResponse { export interface NginxTemplatesResponse extends BaseResponse { items: NginxTemplate[]; } + +export interface UpstreamsResponse extends BaseResponse { + items: Upstream[]; +} diff --git a/frontend/src/components/Table/Formatters.tsx b/frontend/src/components/Table/Formatters.tsx index 8ce459b..554950c 100644 --- a/frontend/src/components/Table/Formatters.tsx +++ b/frontend/src/components/Table/Formatters.tsx @@ -185,6 +185,30 @@ function HostStatusFormatter() { return formatCell; } +function UpstreamStatusFormatter() { + const formatCell = ({ value, row }: any) => { + if (value === "ready") { + return ( + {intl.formatMessage({ id: "ready" })} + ); + } + if (value === "ok") { + return ( + {intl.formatMessage({ id: "ok" })} + ); + } + if (value === "error") { + return ( + + {intl.formatMessage({ id: "error" })} + + ); + } + }; + + return formatCell; +} + function HostTypeFormatter() { const formatCell = ({ value }: any) => { return intl.formatMessage({ id: `host-type.${value}` }); @@ -222,4 +246,5 @@ export { HostTypeFormatter, IDFormatter, SecondsFormatter, + UpstreamStatusFormatter, }; diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index 4c3b92d..4281186 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -8,5 +8,6 @@ export * from "./useHealth"; export * from "./useHosts"; export * from "./useNginxTemplates"; export * from "./useSettings"; +export * from "./useUpstreams"; export * from "./useUser"; export * from "./useUsers"; diff --git a/frontend/src/hooks/useUpstreams.ts b/frontend/src/hooks/useUpstreams.ts new file mode 100644 index 0000000..567e2a8 --- /dev/null +++ b/frontend/src/hooks/useUpstreams.ts @@ -0,0 +1,41 @@ +import { + getUpstreams, + HostsResponse, + tableSortToAPI, + tableFiltersToAPI, +} from "api/npm"; +import { useQuery } from "react-query"; + +const fetchUpstreams = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, +) => { + return getUpstreams( + offset, + limit, + tableSortToAPI(sortBy), + tableFiltersToAPI(filters), + ); +}; + +const useUpstreams = ( + offset = 0, + limit = 10, + sortBy?: any, + filters?: any, + options = {}, +) => { + return useQuery( + ["upstreams", { offset, limit, sortBy, filters }], + () => fetchUpstreams(offset, limit, sortBy, filters), + { + keepPreviousData: true, + staleTime: 15 * 1000, // 15 seconds + ...options, + }, + ); +}; + +export { fetchUpstreams, useUpstreams }; diff --git a/frontend/src/locale/src/de.json b/frontend/src/locale/src/de.json index a204e33..c82b8b5 100644 --- a/frontend/src/locale/src/de.json +++ b/frontend/src/locale/src/de.json @@ -80,6 +80,9 @@ "column.name": { "defaultMessage": "Name" }, + "column.servers": { + "defaultMessage": "Servers" + }, "column.status": { "defaultMessage": "Status" }, @@ -116,6 +119,12 @@ "create-host-title": { "defaultMessage": "Es gibt keine Proxy-Hosts" }, + "create-upstream": { + "defaultMessage": "Create Upstream" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, "dashboard.title": { "defaultMessage": "Armaturenbrett" }, @@ -257,6 +266,9 @@ "no-access": { "defaultMessage": "Kein Zugang" }, + "ok": { + "defaultMessage": "OK" + }, "password.confirm": { "defaultMessage": "Bestätige neues Passwort" }, diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index 560ac02..f48f37f 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -254,6 +254,9 @@ "column.name": { "defaultMessage": "Name" }, + "column.servers": { + "defaultMessage": "Servers" + }, "column.status": { "defaultMessage": "Status" }, @@ -290,6 +293,12 @@ "create-host-title": { "defaultMessage": "There are no Proxy Hosts" }, + "create-upstream": { + "defaultMessage": "Create Upstream" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, "dashboard.title": { "defaultMessage": "Dashboard" }, @@ -446,6 +455,9 @@ "no-access": { "defaultMessage": "No Access" }, + "ok": { + "defaultMessage": "OK" + }, "password.confirm": { "defaultMessage": "Confirm New Password" }, diff --git a/frontend/src/locale/src/fa.json b/frontend/src/locale/src/fa.json index e7180d8..883c341 100644 --- a/frontend/src/locale/src/fa.json +++ b/frontend/src/locale/src/fa.json @@ -80,6 +80,9 @@ "column.name": { "defaultMessage": "نام" }, + "column.servers": { + "defaultMessage": "Servers" + }, "column.status": { "defaultMessage": "وضعیت" }, @@ -116,6 +119,12 @@ "create-host-title": { "defaultMessage": "هیچ هاست پروکسی وجود ندارد" }, + "create-upstream": { + "defaultMessage": "Create Upstream" + }, + "create-upstream-title": { + "defaultMessage": "There are no Upstreams" + }, "dashboard.title": { "defaultMessage": "داشبورد" }, @@ -257,6 +266,9 @@ "no-access": { "defaultMessage": "هیچ دسترسی" }, + "ok": { + "defaultMessage": "OK" + }, "password.confirm": { "defaultMessage": "رمز عبور جدید را تأیید کنید" }, diff --git a/frontend/src/pages/Upstreams/UpstreamsTable.tsx b/frontend/src/pages/Upstreams/UpstreamsTable.tsx new file mode 100644 index 0000000..2156e24 --- /dev/null +++ b/frontend/src/pages/Upstreams/UpstreamsTable.tsx @@ -0,0 +1,156 @@ +import { useEffect, useMemo } from "react"; + +import { + tableEvents, + ActionsFormatter, + GravatarFormatter, + UpstreamStatusFormatter, + IDFormatter, + TableFilter, + TableLayout, + TablePagination, + TableSortBy, + TextFilter, +} from "components"; +import { intl } from "locale"; +import { FiEdit } from "react-icons/fi"; +import { useSortBy, useFilters, useTable, usePagination } from "react-table"; + +const rowActions = [ + { + title: intl.formatMessage({ id: "action.edit" }), + onClick: (e: any, data: any) => { + alert(JSON.stringify(data, null, 2)); + }, + icon: , + show: (data: any) => !data.isSystem, + }, +]; + +export interface UpstreamsTableProps { + data: any; + pagination: TablePagination; + sortBy: TableSortBy[]; + filters: TableFilter[]; + onTableEvent: any; +} +function UpstreamsTable({ + data, + pagination, + onTableEvent, + sortBy, + filters, +}: UpstreamsTableProps) { + const [columns, tableData] = useMemo(() => { + const columns: any[] = [ + { + accessor: "user.gravatarUrl", + Cell: GravatarFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.id" }), + accessor: "id", + Cell: IDFormatter(), + className: "w-80", + }, + { + Header: intl.formatMessage({ id: "column.name" }), + accessor: "name", + sortable: true, + Filter: TextFilter, + }, + { + Header: intl.formatMessage({ id: "column.servers" }), + accessor: "servers.length", + }, + { + Header: intl.formatMessage({ id: "column.status" }), + accessor: "status", + Cell: UpstreamStatusFormatter(), + sortable: true, + }, + { + id: "actions", + accessor: "id", + Cell: ActionsFormatter(rowActions), + className: "w-80", + }, + ]; + return [columns, data]; + }, [data]); + + const tableInstance = useTable( + { + columns, + data: tableData, + initialState: { + pageIndex: Math.floor(pagination.offset / pagination.limit), + pageSize: pagination.limit, + sortBy, + filters, + }, + // Tell the usePagination + // hook that we'll handle our own data fetching + // This means we'll also have to provide our own + // pageCount. + pageCount: Math.ceil(pagination.total / pagination.limit), + manualPagination: true, + // Sorting options + manualSortBy: true, + disableMultiSort: true, + disableSortRemove: true, + autoResetSortBy: false, + // Filter options + manualFilters: true, + autoResetFilters: false, + }, + useFilters, + useSortBy, + usePagination, + ); + + const gotoPage = tableInstance.gotoPage; + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_CHANGED, + payload: tableInstance.state.pageIndex, + }); + }, [onTableEvent, tableInstance.state.pageIndex]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.PAGE_SIZE_CHANGED, + payload: tableInstance.state.pageSize, + }); + gotoPage(0); + }, [gotoPage, onTableEvent, tableInstance.state.pageSize]); + + useEffect(() => { + if (pagination.total) { + onTableEvent({ + type: tableEvents.TOTAL_COUNT_CHANGED, + payload: pagination.total, + }); + } + }, [pagination.total, onTableEvent]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.SORT_CHANGED, + payload: tableInstance.state.sortBy, + }); + }, [onTableEvent, tableInstance.state.sortBy]); + + useEffect(() => { + onTableEvent({ + type: tableEvents.FILTERS_CHANGED, + payload: tableInstance.state.filters, + }); + }, [onTableEvent, tableInstance.state.filters]); + + return ; +} + +export { UpstreamsTable }; diff --git a/frontend/src/pages/Upstreams/index.tsx b/frontend/src/pages/Upstreams/index.tsx new file mode 100644 index 0000000..1cecc9c --- /dev/null +++ b/frontend/src/pages/Upstreams/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useReducer, useState } from "react"; + +import { Alert, AlertIcon, Heading, HStack } from "@chakra-ui/react"; +import { + EmptyList, + PrettyButton, + SpinnerPage, + tableEventReducer, +} from "components"; +import { useUpstreams } from "hooks"; +import { intl } from "locale"; + +import { UpstreamsTable } from "./UpstreamsTable"; + +const initialState = { + offset: 0, + limit: 10, + sortBy: [ + { + id: "name", + desc: false, + }, + ], + filters: [], +}; + +function Upstreams() { + const [{ offset, limit, sortBy, filters }, dispatch] = useReducer( + tableEventReducer, + initialState, + ); + + const [tableData, setTableData] = useState(null); + const { isFetching, isLoading, error, data } = useUpstreams( + offset, + limit, + sortBy, + filters, + ); + + useEffect(() => { + setTableData(data as any); + }, [data]); + + if (error || (!tableData && !isFetching && !isLoading)) { + return ( + + + {error?.message || "Unknown error"} + + ); + } + + if (isFetching || isLoading || !tableData) { + return ; + } + + // When there are no items and no filters active, show the nicer empty view + if (data?.total === 0 && filters?.length === 0) { + return ( + + {intl.formatMessage({ id: "lets-go" })} + + } + /> + ); + } + + const pagination = { + offset: data?.offset || initialState.offset, + limit: data?.limit || initialState.limit, + total: data?.total || 0, + }; + + return ( + <> + + + {intl.formatMessage({ id: "upstreams.title" })} + + + {intl.formatMessage({ id: "create-upstream" })} + + + + + ); +} + +export default Upstreams;