Basis for Upstreams UI
This commit is contained in:
parent
7ea64c46e9
commit
560f3d9b29
@ -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"
|
||||
|
19
frontend/src/api/npm/getUpstreams.ts
Normal file
19
frontend/src/api/npm/getUpstreams.ts
Normal 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;
|
||||
}
|
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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";
|
||||
|
41
frontend/src/hooks/useUpstreams.ts
Normal file
41
frontend/src/hooks/useUpstreams.ts
Normal 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 };
|
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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": "رمز عبور جدید را تأیید کنید"
|
||||
},
|
||||
|
156
frontend/src/pages/Upstreams/UpstreamsTable.tsx
Normal file
156
frontend/src/pages/Upstreams/UpstreamsTable.tsx
Normal 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 };
|
100
frontend/src/pages/Upstreams/index.tsx
Normal file
100
frontend/src/pages/Upstreams/index.tsx
Normal 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;
|
Loading…
Reference in New Issue
Block a user