mirror of
https://github.com/xiaoxinpro/nginx-proxy-manager-zh.git
synced 2025-02-08 20:48:15 -05:00
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 Login = lazy(() => import("pages/Login"));
|
||||||
const GeneralSettings = lazy(() => import("pages/Settings"));
|
const GeneralSettings = lazy(() => import("pages/Settings"));
|
||||||
const Setup = lazy(() => import("pages/Setup"));
|
const Setup = lazy(() => import("pages/Setup"));
|
||||||
|
const Upstreams = lazy(() => import("pages/Upstreams"));
|
||||||
const Users = lazy(() => import("pages/Users"));
|
const Users = lazy(() => import("pages/Users"));
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
@ -56,6 +57,7 @@ function Router() {
|
|||||||
<Suspense fallback={Spinner}>
|
<Suspense fallback={Spinner}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/hosts" element={<Hosts />} />
|
<Route path="/hosts" element={<Hosts />} />
|
||||||
|
<Route path="/upstreams" element={<Upstreams />} />
|
||||||
<Route path="/ssl/certificates" element={<Certificates />} />
|
<Route path="/ssl/certificates" element={<Certificates />} />
|
||||||
<Route
|
<Route
|
||||||
path="/ssl/authorities"
|
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 "./getNginxTemplates";
|
||||||
export * from "./getSettings";
|
export * from "./getSettings";
|
||||||
export * from "./getToken";
|
export * from "./getToken";
|
||||||
|
export * from "./getUpstreams";
|
||||||
export * from "./getUser";
|
export * from "./getUser";
|
||||||
export * from "./getUsers";
|
export * from "./getUsers";
|
||||||
export * from "./helpers";
|
export * from "./helpers";
|
||||||
|
@ -120,3 +120,28 @@ export interface NginxTemplate {
|
|||||||
type: string;
|
type: string;
|
||||||
template: 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,
|
Setting,
|
||||||
Sort,
|
Sort,
|
||||||
User,
|
User,
|
||||||
|
Upstream,
|
||||||
} from "./models";
|
} from "./models";
|
||||||
|
|
||||||
export interface BaseResponse {
|
export interface BaseResponse {
|
||||||
@ -56,3 +57,7 @@ export interface HostsResponse extends BaseResponse {
|
|||||||
export interface NginxTemplatesResponse extends BaseResponse {
|
export interface NginxTemplatesResponse extends BaseResponse {
|
||||||
items: NginxTemplate[];
|
items: NginxTemplate[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpstreamsResponse extends BaseResponse {
|
||||||
|
items: Upstream[];
|
||||||
|
}
|
||||||
|
@ -185,6 +185,30 @@ function HostStatusFormatter() {
|
|||||||
return formatCell;
|
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() {
|
function HostTypeFormatter() {
|
||||||
const formatCell = ({ value }: any) => {
|
const formatCell = ({ value }: any) => {
|
||||||
return intl.formatMessage({ id: `host-type.${value}` });
|
return intl.formatMessage({ id: `host-type.${value}` });
|
||||||
@ -222,4 +246,5 @@ export {
|
|||||||
HostTypeFormatter,
|
HostTypeFormatter,
|
||||||
IDFormatter,
|
IDFormatter,
|
||||||
SecondsFormatter,
|
SecondsFormatter,
|
||||||
|
UpstreamStatusFormatter,
|
||||||
};
|
};
|
||||||
|
@ -8,5 +8,6 @@ export * from "./useHealth";
|
|||||||
export * from "./useHosts";
|
export * from "./useHosts";
|
||||||
export * from "./useNginxTemplates";
|
export * from "./useNginxTemplates";
|
||||||
export * from "./useSettings";
|
export * from "./useSettings";
|
||||||
|
export * from "./useUpstreams";
|
||||||
export * from "./useUser";
|
export * from "./useUser";
|
||||||
export * from "./useUsers";
|
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": {
|
"column.name": {
|
||||||
"defaultMessage": "Name"
|
"defaultMessage": "Name"
|
||||||
},
|
},
|
||||||
|
"column.servers": {
|
||||||
|
"defaultMessage": "Servers"
|
||||||
|
},
|
||||||
"column.status": {
|
"column.status": {
|
||||||
"defaultMessage": "Status"
|
"defaultMessage": "Status"
|
||||||
},
|
},
|
||||||
@ -116,6 +119,12 @@
|
|||||||
"create-host-title": {
|
"create-host-title": {
|
||||||
"defaultMessage": "Es gibt keine Proxy-Hosts"
|
"defaultMessage": "Es gibt keine Proxy-Hosts"
|
||||||
},
|
},
|
||||||
|
"create-upstream": {
|
||||||
|
"defaultMessage": "Create Upstream"
|
||||||
|
},
|
||||||
|
"create-upstream-title": {
|
||||||
|
"defaultMessage": "There are no Upstreams"
|
||||||
|
},
|
||||||
"dashboard.title": {
|
"dashboard.title": {
|
||||||
"defaultMessage": "Armaturenbrett"
|
"defaultMessage": "Armaturenbrett"
|
||||||
},
|
},
|
||||||
@ -257,6 +266,9 @@
|
|||||||
"no-access": {
|
"no-access": {
|
||||||
"defaultMessage": "Kein Zugang"
|
"defaultMessage": "Kein Zugang"
|
||||||
},
|
},
|
||||||
|
"ok": {
|
||||||
|
"defaultMessage": "OK"
|
||||||
|
},
|
||||||
"password.confirm": {
|
"password.confirm": {
|
||||||
"defaultMessage": "Bestätige neues Passwort"
|
"defaultMessage": "Bestätige neues Passwort"
|
||||||
},
|
},
|
||||||
|
@ -254,6 +254,9 @@
|
|||||||
"column.name": {
|
"column.name": {
|
||||||
"defaultMessage": "Name"
|
"defaultMessage": "Name"
|
||||||
},
|
},
|
||||||
|
"column.servers": {
|
||||||
|
"defaultMessage": "Servers"
|
||||||
|
},
|
||||||
"column.status": {
|
"column.status": {
|
||||||
"defaultMessage": "Status"
|
"defaultMessage": "Status"
|
||||||
},
|
},
|
||||||
@ -290,6 +293,12 @@
|
|||||||
"create-host-title": {
|
"create-host-title": {
|
||||||
"defaultMessage": "There are no Proxy Hosts"
|
"defaultMessage": "There are no Proxy Hosts"
|
||||||
},
|
},
|
||||||
|
"create-upstream": {
|
||||||
|
"defaultMessage": "Create Upstream"
|
||||||
|
},
|
||||||
|
"create-upstream-title": {
|
||||||
|
"defaultMessage": "There are no Upstreams"
|
||||||
|
},
|
||||||
"dashboard.title": {
|
"dashboard.title": {
|
||||||
"defaultMessage": "Dashboard"
|
"defaultMessage": "Dashboard"
|
||||||
},
|
},
|
||||||
@ -446,6 +455,9 @@
|
|||||||
"no-access": {
|
"no-access": {
|
||||||
"defaultMessage": "No Access"
|
"defaultMessage": "No Access"
|
||||||
},
|
},
|
||||||
|
"ok": {
|
||||||
|
"defaultMessage": "OK"
|
||||||
|
},
|
||||||
"password.confirm": {
|
"password.confirm": {
|
||||||
"defaultMessage": "Confirm New Password"
|
"defaultMessage": "Confirm New Password"
|
||||||
},
|
},
|
||||||
|
@ -80,6 +80,9 @@
|
|||||||
"column.name": {
|
"column.name": {
|
||||||
"defaultMessage": "نام"
|
"defaultMessage": "نام"
|
||||||
},
|
},
|
||||||
|
"column.servers": {
|
||||||
|
"defaultMessage": "Servers"
|
||||||
|
},
|
||||||
"column.status": {
|
"column.status": {
|
||||||
"defaultMessage": "وضعیت"
|
"defaultMessage": "وضعیت"
|
||||||
},
|
},
|
||||||
@ -116,6 +119,12 @@
|
|||||||
"create-host-title": {
|
"create-host-title": {
|
||||||
"defaultMessage": "هیچ هاست پروکسی وجود ندارد"
|
"defaultMessage": "هیچ هاست پروکسی وجود ندارد"
|
||||||
},
|
},
|
||||||
|
"create-upstream": {
|
||||||
|
"defaultMessage": "Create Upstream"
|
||||||
|
},
|
||||||
|
"create-upstream-title": {
|
||||||
|
"defaultMessage": "There are no Upstreams"
|
||||||
|
},
|
||||||
"dashboard.title": {
|
"dashboard.title": {
|
||||||
"defaultMessage": "داشبورد"
|
"defaultMessage": "داشبورد"
|
||||||
},
|
},
|
||||||
@ -257,6 +266,9 @@
|
|||||||
"no-access": {
|
"no-access": {
|
||||||
"defaultMessage": "هیچ دسترسی"
|
"defaultMessage": "هیچ دسترسی"
|
||||||
},
|
},
|
||||||
|
"ok": {
|
||||||
|
"defaultMessage": "OK"
|
||||||
|
},
|
||||||
"password.confirm": {
|
"password.confirm": {
|
||||||
"defaultMessage": "رمز عبور جدید را تأیید کنید"
|
"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