mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-08-28 03:30:05 +00:00
Certificate Authority work
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import * as api from "./base";
|
||||
import { UserResponse } from "./responseTypes";
|
||||
import { User } from "./models";
|
||||
|
||||
interface AuthOptions {
|
||||
type: string;
|
||||
@@ -20,7 +20,7 @@ interface Options {
|
||||
export async function createUser(
|
||||
{ payload }: Options,
|
||||
abortController?: AbortController,
|
||||
): Promise<UserResponse> {
|
||||
): Promise<User> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/users",
|
||||
|
@@ -1,9 +1,7 @@
|
||||
import * as api from "./base";
|
||||
import { UserResponse } from "./responseTypes";
|
||||
import { User } from "./models";
|
||||
|
||||
export async function getUser(
|
||||
id: number | string = "me",
|
||||
): Promise<UserResponse> {
|
||||
export async function getUser(id: number | string = "me"): Promise<User> {
|
||||
const userId = id ? id : "me";
|
||||
const { result } = await api.get({
|
||||
url: `/users/${userId}`,
|
||||
|
@@ -3,6 +3,9 @@ export * from "./getToken";
|
||||
export * from "./getUser";
|
||||
export * from "./models";
|
||||
export * from "./refreshToken";
|
||||
export * from "./requestCertificateAuthorities";
|
||||
export * from "./requestCertificates";
|
||||
export * from "./requestHealth";
|
||||
export * from "./requestSettings";
|
||||
export * from "./requestUsers";
|
||||
export * from "./responseTypes";
|
||||
|
@@ -3,6 +3,27 @@ export interface Sort {
|
||||
direction: "ASC" | "DESC";
|
||||
}
|
||||
|
||||
export interface UserAuth {
|
||||
id: number;
|
||||
userId: number;
|
||||
type: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
roles: string[];
|
||||
gravatarUrl: string;
|
||||
isDisabled: boolean;
|
||||
auth?: UserAuth;
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
@@ -10,3 +31,27 @@ export interface Setting {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
name: string;
|
||||
acmeshServer: string;
|
||||
caBundle: string;
|
||||
maxDomains: number;
|
||||
isWildcardSupported: boolean;
|
||||
isSetup: boolean;
|
||||
}
|
||||
|
||||
export interface CertificateAuthority {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
name: string;
|
||||
acmeshServer: string;
|
||||
caBundle: string;
|
||||
maxDomains: number;
|
||||
isWildcardSupported: boolean;
|
||||
isSetup: boolean;
|
||||
}
|
||||
|
16
frontend/src/api/npm/requestCertificateAuthorities.ts
Normal file
16
frontend/src/api/npm/requestCertificateAuthorities.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as api from "./base";
|
||||
import { CertificateAuthoritiesResponse } from "./responseTypes";
|
||||
|
||||
export async function requestCertificateAuthorities(
|
||||
offset?: number,
|
||||
abortController?: AbortController,
|
||||
): Promise<CertificateAuthoritiesResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "certificate-authorities",
|
||||
params: { limit: 20, offset: offset || 0 },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
16
frontend/src/api/npm/requestCertificates.ts
Normal file
16
frontend/src/api/npm/requestCertificates.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as api from "./base";
|
||||
import { CertificatesResponse } from "./responseTypes";
|
||||
|
||||
export async function requestCertificates(
|
||||
offset?: number,
|
||||
abortController?: AbortController,
|
||||
): Promise<CertificatesResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "certificates",
|
||||
params: { limit: 20, offset: offset || 0 },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
16
frontend/src/api/npm/requestUsers.ts
Normal file
16
frontend/src/api/npm/requestUsers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as api from "./base";
|
||||
import { UsersResponse } from "./responseTypes";
|
||||
|
||||
export async function requestUsers(
|
||||
offset?: number,
|
||||
abortController?: AbortController,
|
||||
): Promise<UsersResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "users",
|
||||
params: { limit: 20, offset: offset || 0 },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
@@ -1,4 +1,10 @@
|
||||
import { Sort, Setting } from "./models";
|
||||
import {
|
||||
Certificate,
|
||||
CertificateAuthority,
|
||||
Setting,
|
||||
Sort,
|
||||
User,
|
||||
} from "./models";
|
||||
|
||||
export interface HealthResponse {
|
||||
commit: string;
|
||||
@@ -8,32 +14,11 @@ export interface HealthResponse {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface UserAuthResponse {
|
||||
id: number;
|
||||
userId: number;
|
||||
type: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
expires: number;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
createdOn: number;
|
||||
updatedOn: number;
|
||||
roles: string[];
|
||||
gravatarUrl: string;
|
||||
isDisabled: boolean;
|
||||
auth?: UserAuthResponse;
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
total: number;
|
||||
offset: number;
|
||||
@@ -41,3 +26,27 @@ export interface SettingsResponse {
|
||||
sort: Sort[];
|
||||
items: Setting[];
|
||||
}
|
||||
|
||||
export interface CertificatesResponse {
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
sort: Sort[];
|
||||
items: Certificate[];
|
||||
}
|
||||
|
||||
export interface CertificateAuthoritiesResponse {
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
sort: Sort[];
|
||||
items: CertificateAuthority[];
|
||||
}
|
||||
|
||||
export interface UsersResponse {
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
sort: Sort[];
|
||||
items: User[];
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import { Navigation } from "components";
|
||||
import { Dropdown, Navigation } from "components";
|
||||
import { intl } from "locale";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Book,
|
||||
DeviceDesktop,
|
||||
@@ -43,20 +44,30 @@ function NavMenu() {
|
||||
to: "/access-lists",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "certificates.title",
|
||||
defaultMessage: "Certificates",
|
||||
}),
|
||||
title: "SSL",
|
||||
icon: <Shield />,
|
||||
to: "/certificates",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "users.title",
|
||||
defaultMessage: "Users",
|
||||
}),
|
||||
icon: <Users />,
|
||||
to: "/users",
|
||||
dropdownItems: [
|
||||
<Dropdown.Item key="ssl-certificates">
|
||||
<Link to="/ssl/certificates" role="button" aria-expanded="false">
|
||||
<span className="nav-link-title">
|
||||
{intl.formatMessage({
|
||||
id: "certificates.title",
|
||||
defaultMessage: "Certificates",
|
||||
})}
|
||||
</span>
|
||||
</Link>
|
||||
</Dropdown.Item>,
|
||||
<Dropdown.Item key="ssl-authorities">
|
||||
<Link to="/ssl/authorities" role="button" aria-expanded="false">
|
||||
<span className="nav-link-title">
|
||||
{intl.formatMessage({
|
||||
id: "cert_authorities.title",
|
||||
defaultMessage: "Certificate Authorities",
|
||||
})}
|
||||
</span>
|
||||
</Link>
|
||||
</Dropdown.Item>,
|
||||
],
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
@@ -66,6 +77,14 @@ function NavMenu() {
|
||||
icon: <Book />,
|
||||
to: "/audit-log",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "users.title",
|
||||
defaultMessage: "Users",
|
||||
}),
|
||||
icon: <Users />,
|
||||
to: "/users",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: "settings.title",
|
||||
|
@@ -12,6 +12,9 @@ import { BrowserRouter, Switch, Route } from "react-router-dom";
|
||||
const AccessLists = lazy(() => import("pages/AccessLists"));
|
||||
const AuditLog = lazy(() => import("pages/AuditLog"));
|
||||
const Certificates = lazy(() => import("pages/Certificates"));
|
||||
const CertificateAuthorities = lazy(
|
||||
() => import("pages/CertificateAuthorities"),
|
||||
);
|
||||
const Dashboard = lazy(() => import("pages/Dashboard"));
|
||||
const Hosts = lazy(() => import("pages/Hosts"));
|
||||
const Login = lazy(() => import("pages/Login"));
|
||||
@@ -54,9 +57,12 @@ function Router() {
|
||||
<Route path="/hosts">
|
||||
<Hosts />
|
||||
</Route>
|
||||
<Route path="/certificates">
|
||||
<Route path="/ssl/certificates">
|
||||
<Certificates />
|
||||
</Route>
|
||||
<Route path="/ssl/authorities">
|
||||
<CertificateAuthorities />
|
||||
</Route>
|
||||
<Route path="/audit-log">
|
||||
<AuditLog />
|
||||
</Route>
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
import { Badge } from "components";
|
||||
import { intl } from "locale";
|
||||
|
||||
export interface TableColumn {
|
||||
/**
|
||||
@@ -52,9 +54,33 @@ export const Table = ({ columns, data, pagination, sortBy }: TableProps) => {
|
||||
switch (given) {
|
||||
// Simple ID column has text-muted
|
||||
case "id":
|
||||
return (val: any) => {
|
||||
return (val: number) => {
|
||||
return <span className="text-muted">{val}</span>;
|
||||
};
|
||||
case "setup":
|
||||
return (val: boolean) => {
|
||||
return (
|
||||
<Badge color={val ? "lime" : "red"}>
|
||||
{val
|
||||
? intl.formatMessage({
|
||||
id: "ready",
|
||||
defaultMessage: "Ready",
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: "required",
|
||||
defaultMessage: "Required",
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
case "bool":
|
||||
return (val: boolean) => {
|
||||
return (
|
||||
<Badge color={val ? "lime" : "red"}>
|
||||
{val ? "true" : "false"}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { getUser, UserResponse } from "api/npm";
|
||||
import { getUser, User } from "api/npm";
|
||||
import { useAuthState } from "context";
|
||||
|
||||
// Context
|
||||
const initalValue = null;
|
||||
const UserContext = React.createContext<UserResponse | null>(initalValue);
|
||||
const UserContext = React.createContext<User | null>(initalValue);
|
||||
|
||||
// Provider
|
||||
interface Props {
|
||||
children?: JSX.Element;
|
||||
}
|
||||
function UserProvider({ children }: Props) {
|
||||
const [userData, setUserData] = useState<UserResponse>({
|
||||
const [userData, setUserData] = useState<User>({
|
||||
id: 0,
|
||||
name: "",
|
||||
nickname: "",
|
||||
|
@@ -5,6 +5,9 @@
|
||||
"auditlog.title": {
|
||||
"defaultMessage": "Audit Log"
|
||||
},
|
||||
"cert_authorities.title": {
|
||||
"defaultMessage": "Certificate Authorities"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Certificates"
|
||||
},
|
||||
@@ -14,12 +17,21 @@
|
||||
"column.id": {
|
||||
"defaultMessage": "ID"
|
||||
},
|
||||
"column.max_domains": {
|
||||
"defaultMessage": "Domains per Cert"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "Status"
|
||||
},
|
||||
"dashboard.title": {
|
||||
"defaultMessage": "Dashboard"
|
||||
},
|
||||
"column.wildcard_support": {
|
||||
"defaultMessage": "Wildcard Support"
|
||||
},
|
||||
"footer.changelog": {
|
||||
"defaultMessage": "Change Log"
|
||||
},
|
||||
@@ -47,6 +59,12 @@
|
||||
"profile.title": {
|
||||
"defaultMessage": "Profile settings"
|
||||
},
|
||||
"ready": {
|
||||
"defaultMessage": "Ready"
|
||||
},
|
||||
"required": {
|
||||
"defaultMessage": "Required"
|
||||
},
|
||||
"settings.title": {
|
||||
"defaultMessage": "Settings"
|
||||
},
|
||||
|
115
frontend/src/pages/CertificateAuthorities/index.tsx
Normal file
115
frontend/src/pages/CertificateAuthorities/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
|
||||
import {
|
||||
CertificateAuthoritiesResponse,
|
||||
requestCertificateAuthorities,
|
||||
} from "api/npm";
|
||||
import { Table } from "components";
|
||||
import { SuspenseLoader } from "components";
|
||||
import { intl } from "locale";
|
||||
import { useInterval } from "rooks";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
function CertificateAuthorities() {
|
||||
const [data, setData] = useState({} as CertificateAuthoritiesResponse);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const asyncFetch = useCallback(() => {
|
||||
requestCertificateAuthorities(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: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
|
||||
formatter: "id",
|
||||
className: "w-1",
|
||||
},
|
||||
*/
|
||||
{
|
||||
name: "name",
|
||||
title: intl.formatMessage({ id: "column.name", defaultMessage: "Name" }),
|
||||
},
|
||||
{
|
||||
name: "maxDomains",
|
||||
title: intl.formatMessage({
|
||||
id: "column.max_domains",
|
||||
defaultMessage: "Max Domains",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "isWildcardSupported",
|
||||
title: intl.formatMessage({
|
||||
id: "column.wildcard_support",
|
||||
defaultMessage: "Wildcard Support",
|
||||
}),
|
||||
formatter: "bool",
|
||||
},
|
||||
{
|
||||
name: "isSetup",
|
||||
title: intl.formatMessage({
|
||||
id: "column.status",
|
||||
defaultMessage: "Status",
|
||||
}),
|
||||
formatter: "setup",
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof data.total !== "undefined" && data.total) {
|
||||
return (
|
||||
<Root>
|
||||
<div className="card">
|
||||
<div className="card-status-top bg-orange" />
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{intl.formatMessage({
|
||||
id: "cert_authorities.title",
|
||||
defaultMessage: "Certificate Authorities",
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
<Table
|
||||
columns={cols}
|
||||
data={data.items}
|
||||
sortBy={data.sort[0].field}
|
||||
pagination={{
|
||||
limit: data.limit,
|
||||
offset: data.offset,
|
||||
total: data.total,
|
||||
onSetOffset: (num: number) => {
|
||||
if (offset !== num) {
|
||||
setOffset(num);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data.total !== "undefined") {
|
||||
return <p>No items!</p>;
|
||||
}
|
||||
|
||||
return <SuspenseLoader />;
|
||||
}
|
||||
|
||||
export default CertificateAuthorities;
|
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
|
||||
import { CertificatesResponse, requestCertificates } from "api/npm";
|
||||
import { Table } from "components";
|
||||
import { SuspenseLoader } from "components";
|
||||
import { intl } from "locale";
|
||||
import { useInterval } from "rooks";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
@@ -8,22 +12,76 @@ const Root = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
function Certificates() {
|
||||
return (
|
||||
<Root>
|
||||
<div className="card">
|
||||
<div className="card-status-top bg-cyan" />
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{intl.formatMessage({
|
||||
id: "certificates.title",
|
||||
defaultMessage: "Certificates",
|
||||
})}
|
||||
</h3>
|
||||
function CertificateAuthorities() {
|
||||
const [data, setData] = useState({} as CertificatesResponse);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const asyncFetch = useCallback(() => {
|
||||
requestCertificates(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: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
|
||||
formatter: "id",
|
||||
className: "w-1",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
title: intl.formatMessage({ id: "column.name", defaultMessage: "Name" }),
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof data.total !== "undefined" && data.total) {
|
||||
return (
|
||||
<Root>
|
||||
<div className="card">
|
||||
<div className="card-status-top bg-yellow" />
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{intl.formatMessage({
|
||||
id: "certificates.title",
|
||||
defaultMessage: "Certificates",
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
<Table
|
||||
columns={cols}
|
||||
data={data.items}
|
||||
sortBy={data.sort[0].field}
|
||||
pagination={{
|
||||
limit: data.limit,
|
||||
offset: data.offset,
|
||||
total: data.total,
|
||||
onSetOffset: (num: number) => {
|
||||
if (offset !== num) {
|
||||
setOffset(num);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data.total !== "undefined") {
|
||||
return <p>No items!</p>;
|
||||
}
|
||||
|
||||
return <SuspenseLoader />;
|
||||
}
|
||||
|
||||
export default Certificates;
|
||||
export default CertificateAuthorities;
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
|
||||
import { UsersResponse, requestUsers } from "api/npm";
|
||||
import { Table } from "components";
|
||||
import { SuspenseLoader } from "components";
|
||||
import { intl } from "locale";
|
||||
import { useInterval } from "rooks";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Root = styled.div`
|
||||
@@ -9,21 +13,79 @@ const Root = styled.div`
|
||||
`;
|
||||
|
||||
function Users() {
|
||||
return (
|
||||
<Root>
|
||||
<div className="card">
|
||||
<div className="card-status-top bg-cyan" />
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{intl.formatMessage({
|
||||
id: "users.title",
|
||||
defaultMessage: "Users",
|
||||
})}
|
||||
</h3>
|
||||
const [data, setData] = useState({} as UsersResponse);
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const asyncFetch = useCallback(() => {
|
||||
requestUsers(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: intl.formatMessage({ id: "column.id", defaultMessage: "ID" }),
|
||||
formatter: "id",
|
||||
className: "w-1",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
title: intl.formatMessage({ id: "users.name", defaultMessage: "Name" }),
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
title: intl.formatMessage({ id: "users.email", defaultMessage: "Email" }),
|
||||
},
|
||||
];
|
||||
|
||||
if (typeof data.total !== "undefined" && data.total) {
|
||||
return (
|
||||
<Root>
|
||||
<div className="card">
|
||||
<div className="card-status-top bg-indigo" />
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{intl.formatMessage({
|
||||
id: "users.title",
|
||||
defaultMessage: "Users",
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
<Table
|
||||
columns={cols}
|
||||
data={data.items}
|
||||
sortBy={data.sort[0].field}
|
||||
pagination={{
|
||||
limit: data.limit,
|
||||
offset: data.offset,
|
||||
total: data.total,
|
||||
onSetOffset: (num: number) => {
|
||||
if (offset !== num) {
|
||||
setOffset(num);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Root>
|
||||
);
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof data.total !== "undefined") {
|
||||
return <p>No items!</p>;
|
||||
}
|
||||
|
||||
return <SuspenseLoader />;
|
||||
}
|
||||
|
||||
export default Users;
|
||||
|
Reference in New Issue
Block a user