Basis for Upstreams UI

This commit is contained in:
Jamie Curnow 2023-01-10 11:46:40 +10:00
parent 7ea64c46e9
commit 560f3d9b29
13 changed files with 411 additions and 0 deletions

View File

@ -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() {
<Suspense fallback={Spinner}>
<Routes>
<Route path="/hosts" element={<Hosts />} />
<Route path="/upstreams" element={<Upstreams />} />
<Route path="/ssl/certificates" element={<Certificates />} />
<Route
path="/ssl/authorities"

View File

@ -0,0 +1,19 @@
import * as api from "./base";
import { UpstreamsResponse } from "./responseTypes";
export async function getUpstreams(
offset = 0,
limit = 10,
sort?: string,
filters?: { [key: string]: string },
abortController?: AbortController,
): Promise<UpstreamsResponse> {
const { result } = await api.get(
{
url: "upstreams",
params: { limit, offset, sort, expand: "user", ...filters },
},
abortController,
);
return result;
}

View File

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

View File

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

View File

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

View File

@ -185,6 +185,30 @@ function HostStatusFormatter() {
return formatCell;
}
function UpstreamStatusFormatter() {
const formatCell = ({ value, row }: any) => {
if (value === "ready") {
return (
<Badge color="cyan.500">{intl.formatMessage({ id: "ready" })}</Badge>
);
}
if (value === "ok") {
return (
<Badge color="green.500">{intl.formatMessage({ id: "ok" })}</Badge>
);
}
if (value === "error") {
return (
<Tooltip label={row.original.errorMessage}>
<Badge color="red.500">{intl.formatMessage({ id: "error" })}</Badge>
</Tooltip>
);
}
};
return formatCell;
}
function HostTypeFormatter() {
const formatCell = ({ value }: any) => {
return intl.formatMessage({ id: `host-type.${value}` });
@ -222,4 +246,5 @@ export {
HostTypeFormatter,
IDFormatter,
SecondsFormatter,
UpstreamStatusFormatter,
};

View File

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

View File

@ -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<HostsResponse, Error>(
["upstreams", { offset, limit, sortBy, filters }],
() => fetchUpstreams(offset, limit, sortBy, filters),
{
keepPreviousData: true,
staleTime: 15 * 1000, // 15 seconds
...options,
},
);
};
export { fetchUpstreams, useUpstreams };

View File

@ -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"
},

View File

@ -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"
},

View File

@ -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": "رمز عبور جدید را تأیید کنید"
},

View File

@ -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: <FiEdit />,
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 <TableLayout pagination={pagination} {...tableInstance} />;
}
export { UpstreamsTable };

View File

@ -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 (
<Alert status="error">
<AlertIcon />
{error?.message || "Unknown error"}
</Alert>
);
}
if (isFetching || isLoading || !tableData) {
return <SpinnerPage />;
}
// When there are no items and no filters active, show the nicer empty view
if (data?.total === 0 && filters?.length === 0) {
return (
<EmptyList
title={intl.formatMessage({ id: "create-upstream-title" })}
summary={intl.formatMessage({ id: "create-hint" })}
createButton={
<PrettyButton mt={5}>
{intl.formatMessage({ id: "lets-go" })}
</PrettyButton>
}
/>
);
}
const pagination = {
offset: data?.offset || initialState.offset,
limit: data?.limit || initialState.limit,
total: data?.total || 0,
};
return (
<>
<HStack mx={6} my={4} justifyContent="space-between">
<Heading mb={2}>
{intl.formatMessage({ id: "upstreams.title" })}
</Heading>
<PrettyButton size="sm">
{intl.formatMessage({ id: "create-upstream" })}
</PrettyButton>
</HStack>
<UpstreamsTable
data={data?.items || []}
pagination={pagination}
sortBy={sortBy}
filters={filters}
onTableEvent={dispatch}
/>
</>
);
}
export default Upstreams;