Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
11
frontend/src/App.test.tsx
Normal file
11
frontend/src/App.test.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
import * as ReactDOM from "react-dom";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
it("renders without crashing", () => {
|
||||
const div = document.createElement("div");
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
31
frontend/src/App.tsx
Normal file
31
frontend/src/App.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ChakraProvider } from "@chakra-ui/react";
|
||||
import { AuthProvider, LocaleProvider } from "context";
|
||||
import { intl } from "locale";
|
||||
import { RawIntlProvider } from "react-intl";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
|
||||
import Router from "./Router";
|
||||
import lightTheme from "./theme/customTheme";
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<RawIntlProvider value={intl}>
|
||||
<LocaleProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={lightTheme}>
|
||||
<AuthProvider>
|
||||
<Router />
|
||||
</AuthProvider>
|
||||
</ChakraProvider>
|
||||
<ReactQueryDevtools />
|
||||
</QueryClientProvider>
|
||||
</LocaleProvider>
|
||||
</RawIntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
81
frontend/src/Router.tsx
Normal file
81
frontend/src/Router.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
import { SiteWrapper, SpinnerPage, Unhealthy } from "components";
|
||||
import { useAuthState, useLocaleState } from "context";
|
||||
import { useHealth } from "hooks";
|
||||
import { BrowserRouter, Routes, 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 DNSProviders = lazy(() => import("pages/DNSProviders"));
|
||||
const Hosts = lazy(() => import("pages/Hosts"));
|
||||
const HostTemplates = lazy(() => import("pages/HostTemplates"));
|
||||
const Login = lazy(() => import("pages/Login"));
|
||||
const GeneralSettings = lazy(() => import("pages/Settings"));
|
||||
const Setup = lazy(() => import("pages/Setup"));
|
||||
const Users = lazy(() => import("pages/Users"));
|
||||
|
||||
function Router() {
|
||||
const health = useHealth();
|
||||
const { authenticated } = useAuthState();
|
||||
const { locale } = useLocaleState();
|
||||
const Spinner = <SpinnerPage />;
|
||||
|
||||
if (health.isLoading) {
|
||||
return Spinner;
|
||||
}
|
||||
|
||||
if (health.isError || !health.data?.healthy) {
|
||||
return <Unhealthy />;
|
||||
}
|
||||
|
||||
if (health.data?.healthy && !health.data?.setup) {
|
||||
return (
|
||||
<Suspense fallback={Spinner}>
|
||||
<Setup />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Suspense fallback={Spinner}>
|
||||
<Login />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<SiteWrapper key={`locale-${locale}`}>
|
||||
<Suspense fallback={Spinner}>
|
||||
<Routes>
|
||||
<Route path="/hosts" element={<Hosts />} />
|
||||
<Route path="/ssl/certificates" element={<Certificates />} />
|
||||
<Route
|
||||
path="/ssl/authorities"
|
||||
element={<CertificateAuthorities />}
|
||||
/>
|
||||
<Route path="/ssl/dns-providers" element={<DNSProviders />} />
|
||||
<Route path="/audit-log" element={<AuditLog />} />
|
||||
<Route path="/access-lists" element={<AccessLists />} />
|
||||
<Route path="/users" element={<Users />} />
|
||||
<Route
|
||||
path="/settings/host-templates"
|
||||
element={<HostTemplates />}
|
||||
/>
|
||||
<Route path="/settings/general" element={<GeneralSettings />} />
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</SiteWrapper>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default Router;
|
95
frontend/src/api/npm/base.ts
Normal file
95
frontend/src/api/npm/base.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { camelizeKeys, decamelizeKeys } from "humps";
|
||||
import AuthStore from "modules/AuthStore";
|
||||
import * as queryString from "query-string";
|
||||
|
||||
const contentTypeHeader = "Content-Type";
|
||||
|
||||
interface BuildUrlArgs {
|
||||
url: string;
|
||||
params?: queryString.StringifiableRecord;
|
||||
}
|
||||
function buildUrl({ url, params }: BuildUrlArgs) {
|
||||
const endpoint = url.replace(/^\/|\/$/g, "");
|
||||
const apiParams = params ? `?${queryString.stringify(params)}` : "";
|
||||
const apiUrl = `/api/${endpoint}${apiParams}`;
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
function buildAuthHeader(): Record<string, string> | undefined {
|
||||
if (AuthStore.token) {
|
||||
return { Authorization: `Bearer ${AuthStore.token.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function buildBody(data?: Record<string, any>) {
|
||||
if (data) {
|
||||
return JSON.stringify(decamelizeKeys(data));
|
||||
}
|
||||
}
|
||||
|
||||
async function processResponse(response: Response) {
|
||||
const payload = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error.message);
|
||||
}
|
||||
return camelizeKeys(payload) as any;
|
||||
}
|
||||
|
||||
interface GetArgs {
|
||||
url: string;
|
||||
params?: queryString.StringifiableRecord;
|
||||
}
|
||||
|
||||
export async function get(
|
||||
{ url, params }: GetArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url, params });
|
||||
const method = "GET";
|
||||
const signal = abortController?.signal;
|
||||
const headers = buildAuthHeader();
|
||||
const response = await fetch(apiUrl, { method, headers, signal });
|
||||
return processResponse(response);
|
||||
}
|
||||
|
||||
interface PostArgs {
|
||||
url: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export async function post(
|
||||
{ url, data }: PostArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url });
|
||||
const method = "POST";
|
||||
const headers = {
|
||||
...buildAuthHeader(),
|
||||
[contentTypeHeader]: "application/json",
|
||||
};
|
||||
const signal = abortController?.signal;
|
||||
const body = buildBody(data);
|
||||
const response = await fetch(apiUrl, { method, headers, body, signal });
|
||||
return processResponse(response);
|
||||
}
|
||||
|
||||
interface PutArgs {
|
||||
url: string;
|
||||
data?: any;
|
||||
}
|
||||
export async function put(
|
||||
{ url, data }: PutArgs,
|
||||
abortController?: AbortController,
|
||||
) {
|
||||
const apiUrl = buildUrl({ url });
|
||||
const method = "PUT";
|
||||
const headers = {
|
||||
...buildAuthHeader(),
|
||||
[contentTypeHeader]: "application/json",
|
||||
};
|
||||
const signal = abortController?.signal;
|
||||
const body = buildBody(data);
|
||||
const response = await fetch(apiUrl, { method, headers, body, signal });
|
||||
return processResponse(response);
|
||||
}
|
16
frontend/src/api/npm/createCertificateAuthority.ts
Normal file
16
frontend/src/api/npm/createCertificateAuthority.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as api from "./base";
|
||||
import { CertificateAuthority } from "./models";
|
||||
|
||||
export async function createCertificateAuthority(
|
||||
data: CertificateAuthority,
|
||||
abortController?: AbortController,
|
||||
): Promise<CertificateAuthority> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/certificate-authorities",
|
||||
data,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
16
frontend/src/api/npm/createDNSProvider.ts
Normal file
16
frontend/src/api/npm/createDNSProvider.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as api from "./base";
|
||||
import { DNSProvider } from "./models";
|
||||
|
||||
export async function createDNSProvider(
|
||||
data: DNSProvider,
|
||||
abortController?: AbortController,
|
||||
): Promise<DNSProvider> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/dns-providers",
|
||||
data,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
30
frontend/src/api/npm/createUser.ts
Normal file
30
frontend/src/api/npm/createUser.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as api from "./base";
|
||||
import { User } from "./models";
|
||||
|
||||
export interface AuthOptions {
|
||||
type: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
export interface NewUser {
|
||||
name: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
isDisabled: boolean;
|
||||
auth: AuthOptions;
|
||||
capabilities: string[];
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
data: NewUser,
|
||||
abortController?: AbortController,
|
||||
): Promise<User> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/users",
|
||||
data,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getCertificateAuthorities.ts
Normal file
19
frontend/src/api/npm/getCertificateAuthorities.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { CertificateAuthoritiesResponse } from "./responseTypes";
|
||||
|
||||
export async function getCertificateAuthorities(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<CertificateAuthoritiesResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "certificate-authorities",
|
||||
params: { limit, offset, sort, ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
13
frontend/src/api/npm/getCertificateAuthority.ts
Normal file
13
frontend/src/api/npm/getCertificateAuthority.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as api from "./base";
|
||||
import { CertificateAuthority } from "./models";
|
||||
|
||||
export async function getCertificateAuthority(
|
||||
id: number,
|
||||
params = {},
|
||||
): Promise<CertificateAuthority> {
|
||||
const { result } = await api.get({
|
||||
url: `/certificate-authorities/${id}`,
|
||||
params,
|
||||
});
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getCertificates.ts
Normal file
19
frontend/src/api/npm/getCertificates.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { CertificatesResponse } from "./responseTypes";
|
||||
|
||||
export async function getCertificates(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<CertificatesResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "certificates",
|
||||
params: { limit, offset, sort, ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
13
frontend/src/api/npm/getDNSProvider.ts
Normal file
13
frontend/src/api/npm/getDNSProvider.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import * as api from "./base";
|
||||
import { DNSProvider } from "./models";
|
||||
|
||||
export async function getDNSProvider(
|
||||
id: number,
|
||||
params = {},
|
||||
): Promise<DNSProvider> {
|
||||
const { result } = await api.get({
|
||||
url: `/dns-providers/${id}`,
|
||||
params,
|
||||
});
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getDNSProviders.ts
Normal file
19
frontend/src/api/npm/getDNSProviders.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { DNSProvidersResponse } from "./responseTypes";
|
||||
|
||||
export async function getDNSProviders(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<DNSProvidersResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "dns-providers",
|
||||
params: { limit, offset, sort, ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
14
frontend/src/api/npm/getDNSProvidersAcmesh.ts
Normal file
14
frontend/src/api/npm/getDNSProvidersAcmesh.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as api from "./base";
|
||||
import { DNSProvidersAcmesh } from "./models";
|
||||
|
||||
export async function getDNSProvidersAcmesh(
|
||||
abortController?: AbortController,
|
||||
): Promise<DNSProvidersAcmesh[]> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "dns-providers/acmesh",
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
14
frontend/src/api/npm/getHealth.ts
Normal file
14
frontend/src/api/npm/getHealth.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as api from "./base";
|
||||
import { HealthResponse } from "./responseTypes";
|
||||
|
||||
export async function getHealth(
|
||||
abortController?: AbortController,
|
||||
): Promise<HealthResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "",
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getHostTemplates.ts
Normal file
19
frontend/src/api/npm/getHostTemplates.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { HostTemplatesResponse } from "./responseTypes";
|
||||
|
||||
export async function getHostTemplates(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<HostTemplatesResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "host-templates",
|
||||
params: { limit, offset, sort, ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getHosts.ts
Normal file
19
frontend/src/api/npm/getHosts.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { HostsResponse } from "./responseTypes";
|
||||
|
||||
export async function getHosts(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<HostsResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "hosts",
|
||||
params: { limit, offset, sort, expand: "user,certificate", ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getSettings.ts
Normal file
19
frontend/src/api/npm/getSettings.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { SettingsResponse } from "./responseTypes";
|
||||
|
||||
export async function getSettings(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<SettingsResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "settings",
|
||||
params: { limit, offset, sort, ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
24
frontend/src/api/npm/getToken.ts
Normal file
24
frontend/src/api/npm/getToken.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as api from "./base";
|
||||
import { TokenResponse } from "./responseTypes";
|
||||
|
||||
interface Options {
|
||||
payload: {
|
||||
type: string;
|
||||
identity: string;
|
||||
secret: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getToken(
|
||||
{ payload }: Options,
|
||||
abortController?: AbortController,
|
||||
): Promise<TokenResponse> {
|
||||
const { result } = await api.post(
|
||||
{
|
||||
url: "/tokens",
|
||||
data: payload,
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
14
frontend/src/api/npm/getUser.ts
Normal file
14
frontend/src/api/npm/getUser.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as api from "./base";
|
||||
import { User } from "./models";
|
||||
|
||||
export async function getUser(
|
||||
id: number | string = "me",
|
||||
params = {},
|
||||
): Promise<User> {
|
||||
const userId = id ? id : "me";
|
||||
const { result } = await api.get({
|
||||
url: `/users/${userId}`,
|
||||
params,
|
||||
});
|
||||
return result;
|
||||
}
|
19
frontend/src/api/npm/getUsers.ts
Normal file
19
frontend/src/api/npm/getUsers.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import * as api from "./base";
|
||||
import { UsersResponse } from "./responseTypes";
|
||||
|
||||
export async function getUsers(
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sort?: string,
|
||||
filters?: { [key: string]: string },
|
||||
abortController?: AbortController,
|
||||
): Promise<UsersResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "users",
|
||||
params: { limit, offset, sort, expand: "capabilities", ...filters },
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
34
frontend/src/api/npm/helpers.ts
Normal file
34
frontend/src/api/npm/helpers.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { decamelize } from "humps";
|
||||
|
||||
/**
|
||||
* This will convert a react-table sort object into
|
||||
* a string that the backend api likes:
|
||||
* name.asc,id.desc
|
||||
*/
|
||||
export function tableSortToAPI(sortBy: any): string | undefined {
|
||||
if (sortBy?.length > 0) {
|
||||
const strs: string[] = [];
|
||||
sortBy.map((item: any) => {
|
||||
strs.push(decamelize(item.id) + "." + (item.desc ? "desc" : "asc"));
|
||||
return undefined;
|
||||
});
|
||||
return strs.join(",");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will convert a react-table filters object into
|
||||
* a string that the backend api likes:
|
||||
* name:contains=jam
|
||||
*/
|
||||
export function tableFiltersToAPI(filters: any): { [key: string]: string } {
|
||||
const items: { [key: string]: string } = {};
|
||||
if (filters?.length > 0) {
|
||||
filters.map((item: any) => {
|
||||
items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
24
frontend/src/api/npm/index.ts
Normal file
24
frontend/src/api/npm/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export * from "./createCertificateAuthority";
|
||||
export * from "./createDNSProvider";
|
||||
export * from "./createUser";
|
||||
export * from "./getCertificateAuthorities";
|
||||
export * from "./getCertificateAuthority";
|
||||
export * from "./getCertificates";
|
||||
export * from "./getDNSProvider";
|
||||
export * from "./getDNSProviders";
|
||||
export * from "./getDNSProvidersAcmesh";
|
||||
export * from "./getHealth";
|
||||
export * from "./getHosts";
|
||||
export * from "./getHostTemplates";
|
||||
export * from "./getSettings";
|
||||
export * from "./getToken";
|
||||
export * from "./getUser";
|
||||
export * from "./getUsers";
|
||||
export * from "./helpers";
|
||||
export * from "./models";
|
||||
export * from "./refreshToken";
|
||||
export * from "./responseTypes";
|
||||
export * from "./setAuth";
|
||||
export * from "./setCertificateAuthority";
|
||||
export * from "./setDNSProvider";
|
||||
export * from "./setUser";
|
123
frontend/src/api/npm/models.ts
Normal file
123
frontend/src/api/npm/models.ts
Normal file
@ -0,0 +1,123 @@
|
||||
export interface Sort {
|
||||
field: string;
|
||||
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;
|
||||
gravatarUrl: string;
|
||||
isDisabled: boolean;
|
||||
notifications: Notification[];
|
||||
capabilities?: string[];
|
||||
auth?: UserAuth;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
title: string;
|
||||
seen: boolean;
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
// TODO: copy pasta not right
|
||||
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;
|
||||
isReadonly: boolean;
|
||||
}
|
||||
|
||||
export interface DNSProvider {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
acmeshName: string;
|
||||
dnsSleep: number;
|
||||
meta: any;
|
||||
}
|
||||
|
||||
export interface DNSProvidersAcmeshField {
|
||||
name: string;
|
||||
type: string;
|
||||
metaKey: string;
|
||||
isRequired: boolean;
|
||||
isSecret: boolean;
|
||||
}
|
||||
|
||||
export interface DNSProvidersAcmesh {
|
||||
name: string;
|
||||
acmeshName: string;
|
||||
fields: DNSProvidersAcmeshField[];
|
||||
}
|
||||
|
||||
export interface Host {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
userId: number;
|
||||
type: string;
|
||||
hostTemplateId: 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;
|
||||
upstreamOptions: string;
|
||||
advancedConfig: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export interface HostTemplate {
|
||||
id: number;
|
||||
createdOn: number;
|
||||
modifiedOn: number;
|
||||
userId: number;
|
||||
hostType: string;
|
||||
template: string;
|
||||
}
|
14
frontend/src/api/npm/refreshToken.ts
Normal file
14
frontend/src/api/npm/refreshToken.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as api from "./base";
|
||||
import { TokenResponse } from "./responseTypes";
|
||||
|
||||
export async function refreshToken(
|
||||
abortController?: AbortController,
|
||||
): Promise<TokenResponse> {
|
||||
const { result } = await api.get(
|
||||
{
|
||||
url: "/tokens",
|
||||
},
|
||||
abortController,
|
||||
);
|
||||
return result;
|
||||
}
|
58
frontend/src/api/npm/responseTypes.ts
Normal file
58
frontend/src/api/npm/responseTypes.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Certificate,
|
||||
CertificateAuthority,
|
||||
DNSProvider,
|
||||
Host,
|
||||
HostTemplate,
|
||||
Setting,
|
||||
Sort,
|
||||
User,
|
||||
} from "./models";
|
||||
|
||||
export interface BaseResponse {
|
||||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
commit: string;
|
||||
errorReporting: boolean;
|
||||
healthy: boolean;
|
||||
setup: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
expires: number;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SettingsResponse extends BaseResponse {
|
||||
items: Setting[];
|
||||
}
|
||||
|
||||
export interface CertificatesResponse extends BaseResponse {
|
||||
items: Certificate[];
|
||||
}
|
||||
|
||||
export interface CertificateAuthoritiesResponse extends BaseResponse {
|
||||
items: CertificateAuthority[];
|
||||
}
|
||||
|
||||
export interface UsersResponse extends BaseResponse {
|
||||
items: User[];
|
||||
}
|
||||
|
||||
export interface DNSProvidersResponse extends BaseResponse {
|
||||
items: DNSProvider[];
|
||||
}
|
||||
|
||||
export interface HostsResponse extends BaseResponse {
|
||||
items: Host[];
|
||||
}
|
||||
|
||||
export interface HostTemplatesResponse extends BaseResponse {
|
||||
items: HostTemplate[];
|
||||
}
|
18
frontend/src/api/npm/setAuth.ts
Normal file
18
frontend/src/api/npm/setAuth.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import * as api from "./base";
|
||||
import { UserAuth } from "./models";
|
||||
|
||||
export async function setAuth(
|
||||
id: number | string = "me",
|
||||
data: any,
|
||||
): Promise<UserAuth> {
|
||||
const userId = id ? id : "me";
|
||||
if (data.id) {
|
||||
delete data.id;
|
||||
}
|
||||
|
||||
const { result } = await api.post({
|
||||
url: `/users/${userId}/auth`,
|
||||
data,
|
||||
});
|
||||
return result;
|
||||
}
|
17
frontend/src/api/npm/setCertificateAuthority.ts
Normal file
17
frontend/src/api/npm/setCertificateAuthority.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as api from "./base";
|
||||
import { CertificateAuthority } from "./models";
|
||||
|
||||
export async function setCertificateAuthority(
|
||||
id: number,
|
||||
data: any,
|
||||
): Promise<CertificateAuthority> {
|
||||
if (data.id) {
|
||||
delete data.id;
|
||||
}
|
||||
|
||||
const { result } = await api.put({
|
||||
url: `/certificate-authorities/${id}`,
|
||||
data,
|
||||
});
|
||||
return result;
|
||||
}
|
17
frontend/src/api/npm/setDNSProvider.ts
Normal file
17
frontend/src/api/npm/setDNSProvider.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as api from "./base";
|
||||
import { DNSProvider } from "./models";
|
||||
|
||||
export async function setDNSProvider(
|
||||
id: number,
|
||||
data: any,
|
||||
): Promise<DNSProvider> {
|
||||
if (data.id) {
|
||||
delete data.id;
|
||||
}
|
||||
|
||||
const { result } = await api.put({
|
||||
url: `/dns-providers/${id}`,
|
||||
data,
|
||||
});
|
||||
return result;
|
||||
}
|
18
frontend/src/api/npm/setUser.ts
Normal file
18
frontend/src/api/npm/setUser.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import * as api from "./base";
|
||||
import { User } from "./models";
|
||||
|
||||
export async function setUser(
|
||||
id: number | string = "me",
|
||||
data: any,
|
||||
): Promise<User> {
|
||||
const userId = id ? id : "me";
|
||||
if (data.id) {
|
||||
delete data.id;
|
||||
}
|
||||
|
||||
const { result } = await api.put({
|
||||
url: `/users/${userId}`,
|
||||
data,
|
||||
});
|
||||
return result;
|
||||
}
|
23
frontend/src/components/EmptyList.tsx
Normal file
23
frontend/src/components/EmptyList.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Box, Heading, Text } from "@chakra-ui/react";
|
||||
|
||||
interface EmptyListProps {
|
||||
title: string;
|
||||
summary: string;
|
||||
createButton?: ReactNode;
|
||||
}
|
||||
|
||||
function EmptyList({ title, summary, createButton }: EmptyListProps) {
|
||||
return (
|
||||
<Box textAlign="center" py={10} px={6}>
|
||||
<Heading as="h4" size="md" mt={6} mb={2}>
|
||||
{title}
|
||||
</Heading>
|
||||
<Text color="gray.500">{summary}</Text>
|
||||
{createButton}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { EmptyList };
|
32
frontend/src/components/Flag/Flag.tsx
Normal file
32
frontend/src/components/Flag/Flag.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Box } from "@chakra-ui/layout";
|
||||
import { hasFlag } from "country-flag-icons";
|
||||
// @ts-ignore Creating a typing for a subfolder is not easily possible
|
||||
import Flags from "country-flag-icons/react/3x2";
|
||||
|
||||
interface FlagProps {
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Two letter country code of flag
|
||||
*/
|
||||
countryCode: string;
|
||||
}
|
||||
function Flag({ className, countryCode }: FlagProps) {
|
||||
countryCode = countryCode.toUpperCase();
|
||||
|
||||
if (hasFlag(countryCode)) {
|
||||
// @ts-ignore have to do this because of above
|
||||
const FlagElement = Flags[countryCode] as any;
|
||||
return (
|
||||
<Box as={FlagElement} title={countryCode} className={className} w={6} />
|
||||
);
|
||||
} else {
|
||||
console.error(`No flag for country ${countryCode} found!`);
|
||||
|
||||
return <Box w={6} h={4} />;
|
||||
}
|
||||
}
|
||||
|
||||
export { Flag };
|
1
frontend/src/components/Flag/index.ts
Normal file
1
frontend/src/components/Flag/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Flag";
|
64
frontend/src/components/Footer.tsx
Normal file
64
frontend/src/components/Footer.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Link,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<Box
|
||||
bg={useColorModeValue("gray.50", "gray.900")}
|
||||
color={useColorModeValue("gray.700", "gray.200")}>
|
||||
<Container
|
||||
as={Stack}
|
||||
maxW="6xl"
|
||||
py={4}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
spacing={4}
|
||||
justify={{ base: "center", md: "space-between" }}
|
||||
align={{ base: "center", md: "center" }}>
|
||||
<Text>
|
||||
{intl.formatMessage(
|
||||
{ id: "footer.copyright" },
|
||||
{ year: new Date().getFullYear() },
|
||||
)}
|
||||
</Text>
|
||||
<Stack direction="row" spacing={6}>
|
||||
<Link
|
||||
href="https://nginxproxymanager.com?utm_source=npm"
|
||||
isExternal
|
||||
rel="noopener noreferrer">
|
||||
{intl.formatMessage({ id: "footer.userguide" })}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases?utm_source=npm"
|
||||
isExternal
|
||||
rel="noopener noreferrer">
|
||||
{intl.formatMessage({ id: "footer.changelog" })}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/NginxProxyManager/nginx-proxy-manager?utm_source=npm"
|
||||
isExternal
|
||||
rel="noopener noreferrer">
|
||||
{intl.formatMessage({ id: "footer.github" })}
|
||||
</Link>
|
||||
<Tooltip label={process.env.REACT_APP_COMMIT}>
|
||||
<Link
|
||||
href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases?utm_source=npm"
|
||||
isExternal
|
||||
rel="noopener noreferrer">
|
||||
v{process.env.REACT_APP_VERSION}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { Footer };
|
56
frontend/src/components/HelpDrawer/HelpDrawer.tsx
Normal file
56
frontend/src/components/HelpDrawer/HelpDrawer.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerOverlay,
|
||||
DrawerBody,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { getLocale } from "locale";
|
||||
import { FiHelpCircle } from "react-icons/fi";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { getHelpFile } from "../../locale/src/HelpDoc";
|
||||
|
||||
interface HelpDrawerProps {
|
||||
/**
|
||||
* Section to show
|
||||
*/
|
||||
section: string;
|
||||
}
|
||||
function HelpDrawer({ section }: HelpDrawerProps) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [markdownText, setMarkdownText] = useState("");
|
||||
const lang = getLocale(true);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const docFile = getHelpFile(lang, section) as any;
|
||||
fetch(docFile)
|
||||
.then((response) => response.text())
|
||||
.then(setMarkdownText);
|
||||
} catch (ex: any) {
|
||||
setMarkdownText(`**ERROR:** ${ex.message}`);
|
||||
}
|
||||
}, [lang, section]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" onClick={onOpen}>
|
||||
<FiHelpCircle />
|
||||
</Button>
|
||||
<Drawer onClose={onClose} isOpen={isOpen} size="xl">
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
<DrawerBody className="helpdoc-body">
|
||||
<ReactMarkdown>{markdownText}</ReactMarkdown>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { HelpDrawer };
|
1
frontend/src/components/HelpDrawer/index.ts
Normal file
1
frontend/src/components/HelpDrawer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./HelpDrawer";
|
19
frontend/src/components/Loader/Loader.tsx
Normal file
19
frontend/src/components/Loader/Loader.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import cn from "classnames";
|
||||
|
||||
interface LoaderProps {
|
||||
/**
|
||||
* Child elements within
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* Additional Class
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
function Loader({ children, className }: LoaderProps) {
|
||||
return <div className={cn({ loader: true }, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export { Loader };
|
1
frontend/src/components/Loader/index.ts
Normal file
1
frontend/src/components/Loader/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Loader";
|
12
frontend/src/components/Loading.tsx
Normal file
12
frontend/src/components/Loading.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { Loader } from "components";
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<Box textAlign="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { Loading };
|
62
frontend/src/components/LocalePicker.tsx
Normal file
62
frontend/src/components/LocalePicker.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import {
|
||||
Button,
|
||||
Box,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
} from "@chakra-ui/react";
|
||||
import { Flag } from "components";
|
||||
import { useLocaleState } from "context";
|
||||
import {
|
||||
changeLocale,
|
||||
getFlagCodeForLocale,
|
||||
intl,
|
||||
localeOptions,
|
||||
} from "locale";
|
||||
|
||||
interface LocalPickerProps {
|
||||
/**
|
||||
* On change handler
|
||||
*/
|
||||
onChange?: any;
|
||||
/**
|
||||
* Class
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function LocalePicker({ onChange, className }: LocalPickerProps) {
|
||||
const { locale, setLocale } = useLocaleState();
|
||||
|
||||
const changeTo = (lang: string) => {
|
||||
changeLocale(lang);
|
||||
setLocale(lang);
|
||||
onChange && onChange(locale);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
<Menu>
|
||||
<MenuButton as={Button}>
|
||||
<Flag countryCode={getFlagCodeForLocale(locale)} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{localeOptions.map((item) => {
|
||||
return (
|
||||
<MenuItem
|
||||
icon={<Flag countryCode={getFlagCodeForLocale(item[0])} />}
|
||||
onClick={() => changeTo(item[0])}
|
||||
// rel={item[1]}
|
||||
key={`locale-${item[0]}`}>
|
||||
<span>{intl.formatMessage({ id: `locale-${item[1]}` })}</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { LocalePicker };
|
24
frontend/src/components/Navigation/Navigation.tsx
Normal file
24
frontend/src/components/Navigation/Navigation.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useDisclosure } from "@chakra-ui/react";
|
||||
import { NavigationHeader, NavigationMenu } from "components";
|
||||
|
||||
function Navigation() {
|
||||
const {
|
||||
isOpen: mobileNavIsOpen,
|
||||
onToggle: mobileNavToggle,
|
||||
onClose: mobileNavClose,
|
||||
} = useDisclosure();
|
||||
return (
|
||||
<>
|
||||
<NavigationHeader
|
||||
toggleMobileNav={mobileNavToggle}
|
||||
mobileNavIsOpen={mobileNavIsOpen}
|
||||
/>
|
||||
<NavigationMenu
|
||||
mobileNavIsOpen={mobileNavIsOpen}
|
||||
closeMobileNav={mobileNavClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Navigation };
|
113
frontend/src/components/Navigation/NavigationHeader.tsx
Normal file
113
frontend/src/components/Navigation/NavigationHeader.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
chakra,
|
||||
Container,
|
||||
Flex,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ThemeSwitcher } from "components";
|
||||
import { useAuthState } from "context";
|
||||
import { useUser } from "hooks";
|
||||
import { intl } from "locale";
|
||||
import { ChangePasswordModal, ProfileModal } from "modals";
|
||||
import { FiLock, FiLogOut, FiMenu, FiUser, FiX } from "react-icons/fi";
|
||||
|
||||
interface NavigationHeaderProps {
|
||||
mobileNavIsOpen: boolean;
|
||||
toggleMobileNav: () => void;
|
||||
}
|
||||
function NavigationHeader({
|
||||
mobileNavIsOpen,
|
||||
toggleMobileNav,
|
||||
}: NavigationHeaderProps) {
|
||||
const passwordDisclosure = useDisclosure();
|
||||
const profileDisclosure = useDisclosure();
|
||||
const { data: user } = useUser("me");
|
||||
const { logout } = useAuthState();
|
||||
|
||||
return (
|
||||
<Box
|
||||
h={16}
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
<Container h="full" maxW="container.xl">
|
||||
<Flex h="full" alignItems="center" justifyContent="space-between">
|
||||
<IconButton
|
||||
display={{ base: "block", md: "none" }}
|
||||
position="relative"
|
||||
bg="transparent"
|
||||
aria-label={
|
||||
mobileNavIsOpen
|
||||
? intl.formatMessage({ id: "navigation.close" })
|
||||
: intl.formatMessage({ id: "navigation.open" })
|
||||
}
|
||||
onClick={toggleMobileNav}
|
||||
icon={<Icon as={mobileNavIsOpen ? FiX : FiMenu} />}
|
||||
/>
|
||||
<HStack height="full" paddingY={3} spacing={4}>
|
||||
<chakra.img src="/images/logo-no-text.svg" alt="" height="full" />
|
||||
<Text
|
||||
display={{ base: "none", md: "block" }}
|
||||
fontSize="2xl"
|
||||
fontWeight="bold">
|
||||
{intl.formatMessage({ id: "brand.name" })}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<ThemeSwitcher background="transparent" />
|
||||
<Box pl={2}>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded="full"
|
||||
variant="link"
|
||||
cursor="pointer"
|
||||
minW={0}>
|
||||
<Avatar size="sm" src={user?.gravatarUrl} />
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
icon={<Icon as={FiUser} />}
|
||||
onClick={profileDisclosure.onOpen}>
|
||||
{intl.formatMessage({ id: "profile.title" })}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Icon as={FiLock} />}
|
||||
onClick={passwordDisclosure.onOpen}>
|
||||
{intl.formatMessage({ id: "change-password" })}
|
||||
</MenuItem>
|
||||
<MenuDivider />
|
||||
<MenuItem onClick={logout} icon={<Icon as={FiLogOut} />}>
|
||||
{intl.formatMessage({ id: "profile.logout" })}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Container>
|
||||
<ProfileModal
|
||||
isOpen={profileDisclosure.isOpen}
|
||||
onClose={profileDisclosure.onClose}
|
||||
/>
|
||||
<ChangePasswordModal
|
||||
isOpen={passwordDisclosure.isOpen}
|
||||
onClose={passwordDisclosure.onClose}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { NavigationHeader };
|
331
frontend/src/components/Navigation/NavigationMenu.tsx
Normal file
331
frontend/src/components/Navigation/NavigationMenu.tsx
Normal file
@ -0,0 +1,331 @@
|
||||
import { FC, useCallback, useMemo, ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
Flex,
|
||||
forwardRef,
|
||||
HStack,
|
||||
Icon,
|
||||
Link,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Text,
|
||||
Stack,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
Container,
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
import {
|
||||
FiHome,
|
||||
FiSettings,
|
||||
FiUser,
|
||||
FiBook,
|
||||
FiLock,
|
||||
FiShield,
|
||||
FiMonitor,
|
||||
FiChevronDown,
|
||||
} from "react-icons/fi";
|
||||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
|
||||
interface NavItem {
|
||||
/** Displayed label */
|
||||
label: string;
|
||||
/** Icon shown before the label */
|
||||
icon: ReactNode;
|
||||
/** Link where to navigate to */
|
||||
to?: string;
|
||||
subItems?: { label: string; to: string }[];
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{
|
||||
label: intl.formatMessage({ id: "dashboard.title" }),
|
||||
icon: <Icon as={FiHome} />,
|
||||
to: "/",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "hosts.title" }),
|
||||
icon: <Icon as={FiMonitor} />,
|
||||
to: "/hosts",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "access-lists.title" }),
|
||||
icon: <Icon as={FiLock} />,
|
||||
to: "/access-lists",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "ssl.title" }),
|
||||
icon: <Icon as={FiShield} />,
|
||||
subItems: [
|
||||
{
|
||||
label: intl.formatMessage({ id: "certificates.title" }),
|
||||
to: "/ssl/certificates",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "certificate-authorities.title" }),
|
||||
to: "/ssl/authorities",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "dns-providers.title" }),
|
||||
to: "/ssl/dns-providers",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "audit-log.title" }),
|
||||
icon: <Icon as={FiBook} />,
|
||||
to: "/audit-log",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "users.title" }),
|
||||
icon: <Icon as={FiUser} />,
|
||||
to: "/users",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "settings.title" }),
|
||||
icon: <Icon as={FiSettings} />,
|
||||
subItems: [
|
||||
{
|
||||
label: intl.formatMessage({ id: "general-settings.title" }),
|
||||
to: "/settings/general",
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: "host-templates.title" }),
|
||||
to: "/settings/host-templates",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
interface NavigationMenuProps {
|
||||
/** Navigation is currently hidden on mobile */
|
||||
mobileNavIsOpen: boolean;
|
||||
closeMobileNav: () => void;
|
||||
}
|
||||
function NavigationMenu({
|
||||
mobileNavIsOpen,
|
||||
closeMobileNav,
|
||||
}: NavigationMenuProps) {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<Collapse in={mobileNavIsOpen}>
|
||||
<MobileNavigation closeMobileNav={closeMobileNav} />
|
||||
</Collapse>
|
||||
) : (
|
||||
<DesktopNavigation />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Single tab element for desktop navigation */
|
||||
type NavTabProps = Omit<NavItem, "subItems"> & { active?: boolean };
|
||||
const NavTab = forwardRef<NavTabProps, "a">(
|
||||
({ label, icon, to, active, ...props }, ref) => {
|
||||
const linkColor = useColorModeValue("gray.500", "gray.200");
|
||||
const linkHoverColor = useColorModeValue("gray.900", "white");
|
||||
return (
|
||||
<Link
|
||||
as={RouterLink}
|
||||
ref={ref}
|
||||
height={12}
|
||||
to={to ?? "#"}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
borderBottom="1px solid"
|
||||
borderBottomColor={active ? linkHoverColor : "transparent"}
|
||||
color={active ? linkHoverColor : linkColor}
|
||||
_hover={{
|
||||
textDecoration: "none",
|
||||
color: linkHoverColor,
|
||||
borderBottomColor: linkHoverColor,
|
||||
}}
|
||||
{...props}>
|
||||
{icon}
|
||||
<Text as="span" marginLeft={2}>
|
||||
{label}
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const DesktopNavigation: FC = () => {
|
||||
const path = useLocation().pathname;
|
||||
const activeNavItemIndex = useMemo(
|
||||
() =>
|
||||
navItems.findIndex((item) => {
|
||||
// Find the nav item whose location / sub items location is the beginning of the currently active path
|
||||
if (item.to) {
|
||||
// console.debug(item.to, path);
|
||||
if (item.to === "/") {
|
||||
return path === item.to;
|
||||
}
|
||||
return path.startsWith(item.to !== "" ? item.to : "/dashboard");
|
||||
} else if (item.subItems) {
|
||||
return item.subItems.some((subItem) => path.startsWith(subItem.to));
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
[path],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={{ base: "none", md: "block" }}
|
||||
overflowY="visible"
|
||||
overflowX="auto"
|
||||
whiteSpace="nowrap"
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
<Container h="full" maxW="container.xl">
|
||||
<HStack spacing={8}>
|
||||
{navItems.map((navItem, index) => {
|
||||
const { subItems, ...propsWithoutSubItems } = navItem;
|
||||
const additionalProps: Partial<NavTabProps> = {};
|
||||
if (index === activeNavItemIndex) {
|
||||
additionalProps["active"] = true;
|
||||
}
|
||||
if (subItems) {
|
||||
return (
|
||||
<Menu key={`mainnav${index}`}>
|
||||
<MenuButton
|
||||
as={NavTab}
|
||||
{...propsWithoutSubItems}
|
||||
{...additionalProps}
|
||||
/>
|
||||
{subItems && (
|
||||
<MenuList>
|
||||
{subItems.map((item, subIndex) => (
|
||||
<MenuItem
|
||||
as={RouterLink}
|
||||
to={item.to}
|
||||
key={`mainnav${index}-${subIndex}`}>
|
||||
{item.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
)}
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NavTab
|
||||
key={`mainnav${index}`}
|
||||
{...propsWithoutSubItems}
|
||||
{...additionalProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</HStack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavigation: FC<Pick<NavigationMenuProps, "closeMobileNav">> = ({
|
||||
closeMobileNav,
|
||||
}) => {
|
||||
return (
|
||||
<Stack
|
||||
p={4}
|
||||
display={{ md: "none" }}
|
||||
borderBottom="1px solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}>
|
||||
{navItems.map((navItem, index) => (
|
||||
<MobileNavItem
|
||||
key={`mainmobilenav${index}`}
|
||||
index={index}
|
||||
closeMobileNav={closeMobileNav}
|
||||
{...navItem}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileNavItem: FC<
|
||||
NavItem & {
|
||||
index: number;
|
||||
closeMobileNav: NavigationMenuProps["closeMobileNav"];
|
||||
}
|
||||
> = ({ closeMobileNav, ...props }) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
|
||||
const onClickHandler = useCallback(() => {
|
||||
if (props.subItems) {
|
||||
// Toggle accordeon
|
||||
onToggle();
|
||||
} else {
|
||||
// Close menu on navigate
|
||||
closeMobileNav();
|
||||
}
|
||||
}, [closeMobileNav, onToggle, props.subItems]);
|
||||
|
||||
return (
|
||||
<Stack spacing={4} onClick={onClickHandler}>
|
||||
<Box>
|
||||
<Flex
|
||||
py={2}
|
||||
as={RouterLink}
|
||||
to={props.to ?? "#"}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
_hover={{
|
||||
textDecoration: "none",
|
||||
}}>
|
||||
<Box display="flex" alignItems="center">
|
||||
{props.icon}
|
||||
<Text as="span" marginLeft={2}>
|
||||
{props.label}
|
||||
</Text>
|
||||
</Box>
|
||||
{props.subItems && (
|
||||
<Icon
|
||||
as={FiChevronDown}
|
||||
transition="all .25s ease-in-out"
|
||||
transform={isOpen ? "rotate(180deg)" : ""}
|
||||
w={6}
|
||||
h={6}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Collapse
|
||||
in={isOpen}
|
||||
animateOpacity
|
||||
style={{ marginTop: "0 !important" }}>
|
||||
<Stack
|
||||
mt={1}
|
||||
pl={4}
|
||||
borderLeft={1}
|
||||
borderStyle="solid"
|
||||
borderColor={useColorModeValue("gray.200", "gray.700")}
|
||||
align="start">
|
||||
{props.subItems &&
|
||||
props.subItems.map((subItem, subIndex) => (
|
||||
<Link
|
||||
as={RouterLink}
|
||||
key={`mainmobilenav${props.index}-${subIndex}`}
|
||||
py={2}
|
||||
onClick={closeMobileNav}
|
||||
to={subItem.to}>
|
||||
{subItem.label}
|
||||
</Link>
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export { NavigationMenu };
|
3
frontend/src/components/Navigation/index.ts
Normal file
3
frontend/src/components/Navigation/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./Navigation";
|
||||
export * from "./NavigationHeader";
|
||||
export * from "./NavigationMenu";
|
@ -0,0 +1,36 @@
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
import { Heading, Stack, Text, useColorModeValue } from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
|
||||
interface AdminPermissionSelectorProps {
|
||||
selected?: boolean;
|
||||
onClick: MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
function AdminPermissionSelector({
|
||||
selected,
|
||||
onClick,
|
||||
}: AdminPermissionSelectorProps) {
|
||||
return (
|
||||
<Stack
|
||||
onClick={onClick}
|
||||
style={{ cursor: "pointer", opacity: selected ? 1 : 0.4 }}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
w={{ sm: "100%" }}
|
||||
mb={2}
|
||||
p={4}
|
||||
bg={useColorModeValue("white", "gray.900")}
|
||||
boxShadow={selected ? "2xl" : "base"}>
|
||||
<Heading fontSize="2xl" fontFamily="body">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</Heading>
|
||||
<Text color={useColorModeValue("gray.700", "gray.400")}>
|
||||
{intl.formatMessage({ id: "full-access.description" })}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export { AdminPermissionSelector };
|
285
frontend/src/components/Permissions/PermissionSelector.tsx
Normal file
285
frontend/src/components/Permissions/PermissionSelector.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
import { ChangeEvent, MouseEventHandler } from "react";
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
|
||||
interface PermissionSelectorProps {
|
||||
capabilities: string[];
|
||||
selected?: boolean;
|
||||
onClick: MouseEventHandler<HTMLElement>;
|
||||
onChange: (i: string[]) => any;
|
||||
}
|
||||
|
||||
function PermissionSelector({
|
||||
capabilities,
|
||||
selected,
|
||||
onClick,
|
||||
onChange,
|
||||
}: PermissionSelectorProps) {
|
||||
const textColor = useColorModeValue("gray.700", "gray.400");
|
||||
|
||||
const onSelectChange = ({ target }: ChangeEvent<HTMLSelectElement>) => {
|
||||
// remove all items starting with target.name
|
||||
const i: string[] = [];
|
||||
const re = new RegExp(`^${target.name}\\.`, "g");
|
||||
capabilities.forEach((capability) => {
|
||||
if (!capability.match(re)) {
|
||||
i.push(capability);
|
||||
}
|
||||
});
|
||||
|
||||
// add a new item, if value is something, and doesn't already exist
|
||||
if (target.value) {
|
||||
const c = `${target.name}.${target.value}`;
|
||||
if (i.indexOf(c) === -1) {
|
||||
i.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(i);
|
||||
};
|
||||
|
||||
const getDefaultValue = (c: string): string => {
|
||||
if (capabilities.indexOf(`${c}.manage`) !== -1) {
|
||||
return "manage";
|
||||
}
|
||||
if (capabilities.indexOf(`${c}.view`) !== -1) {
|
||||
return "view";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
onClick={onClick}
|
||||
style={{ cursor: "pointer", opacity: selected ? 1 : 0.4 }}
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
w={{ sm: "100%" }}
|
||||
p={4}
|
||||
bg={useColorModeValue("white", "gray.900")}
|
||||
boxShadow={selected ? "2xl" : "base"}>
|
||||
<Heading fontSize="2xl" fontFamily="body">
|
||||
{intl.formatMessage({ id: "restricted-access" })}
|
||||
</Heading>
|
||||
{selected ? (
|
||||
<Stack spacing={3}>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "access-lists.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("access-lists")}
|
||||
onChange={onSelectChange}
|
||||
name="access-lists"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "audit-log.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("audit-log")}
|
||||
onChange={onSelectChange}
|
||||
name="audit-log"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "certificates.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("certificates")}
|
||||
onChange={onSelectChange}
|
||||
name="certificates"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "certificate-authorities.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("certificate-authorities")}
|
||||
onChange={onSelectChange}
|
||||
name="certificate-authorities"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "dns-providers.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("dns-providers")}
|
||||
onChange={onSelectChange}
|
||||
name="dns-providers"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>{intl.formatMessage({ id: "hosts.title" })}</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("hosts")}
|
||||
onChange={onSelectChange}
|
||||
name="hosts"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>
|
||||
{intl.formatMessage({ id: "host-templates.title" })}
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("host-templates")}
|
||||
onChange={onSelectChange}
|
||||
name="host-templates"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
<option value="view">
|
||||
{intl.formatMessage({ id: "view-only" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>{intl.formatMessage({ id: "settings.title" })}</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("settings")}
|
||||
onChange={onSelectChange}
|
||||
name="settings"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Stack direction={{ base: "column", md: "row" }}>
|
||||
<Flex flex={1}>{intl.formatMessage({ id: "users.title" })}</Flex>
|
||||
<Flex flex={1}>
|
||||
<Select
|
||||
defaultValue={getDefaultValue("users")}
|
||||
onChange={onSelectChange}
|
||||
name="users"
|
||||
size="sm"
|
||||
variant="filled"
|
||||
disabled={!selected}>
|
||||
<option value="">
|
||||
{intl.formatMessage({ id: "no-access" })}
|
||||
</option>
|
||||
<option value="manage">
|
||||
{intl.formatMessage({ id: "full-access" })}
|
||||
</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text color={textColor}>
|
||||
{intl.formatMessage({ id: "restricted-access.description" })}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export { PermissionSelector };
|
2
frontend/src/components/Permissions/index.ts
Normal file
2
frontend/src/components/Permissions/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./AdminPermissionSelector";
|
||||
export * from "./PermissionSelector";
|
20
frontend/src/components/PrettyButton.tsx
Normal file
20
frontend/src/components/PrettyButton.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Button, ButtonProps } from "@chakra-ui/react";
|
||||
|
||||
function PrettyButton(props: ButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
fontFamily="heading"
|
||||
bgGradient="linear(to-r, red.400,pink.400)"
|
||||
color="white"
|
||||
_hover={{
|
||||
bgGradient: "linear(to-r, red.400,pink.400)",
|
||||
boxShadow: "xl",
|
||||
}}
|
||||
{...props}>
|
||||
{props.children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { PrettyButton };
|
32
frontend/src/components/SiteWrapper.tsx
Normal file
32
frontend/src/components/SiteWrapper.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Box, Container } from "@chakra-ui/react";
|
||||
import { Footer, Navigation } from "components";
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
function SiteWrapper({ children }: Props) {
|
||||
return (
|
||||
<Box display="flex" flexDir="column" height="100vh">
|
||||
<Box flexShrink={0}>
|
||||
<Navigation />
|
||||
</Box>
|
||||
<Box flex="1 0 auto" overflow="auto">
|
||||
<Container
|
||||
as="main"
|
||||
maxW="container.xl"
|
||||
overflowY="auto"
|
||||
py={4}
|
||||
h="full">
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
<Box flexShrink={0}>
|
||||
<Footer />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export { SiteWrapper };
|
9
frontend/src/components/SpinnerPage.tsx
Normal file
9
frontend/src/components/SpinnerPage.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Flex, Spinner } from "@chakra-ui/react";
|
||||
|
||||
export function SpinnerPage() {
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center" h="full">
|
||||
<Spinner />
|
||||
</Flex>
|
||||
);
|
||||
}
|
225
frontend/src/components/Table/Formatters.tsx
Normal file
225
frontend/src/components/Table/Formatters.tsx
Normal file
@ -0,0 +1,225 @@
|
||||
import { Avatar, Badge, Text, Tooltip } from "@chakra-ui/react";
|
||||
import { RowAction, RowActionsMenu } from "components";
|
||||
import { intl } from "locale";
|
||||
import getNiceDNSProvider from "modules/Acmesh";
|
||||
|
||||
function ActionsFormatter(rowActions: RowAction[]) {
|
||||
const formatCell = (instance: any) => {
|
||||
return <RowActionsMenu data={instance.row.original} actions={rowActions} />;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function BooleanFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return (
|
||||
<Badge color={value ? "cyan.500" : "red.400"}>
|
||||
{value ? "true" : "false"}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function CapabilitiesFormatter() {
|
||||
const formatCell = ({ row, value }: any) => {
|
||||
const style = {} as any;
|
||||
if (row?.original?.isDisabled) {
|
||||
style.textDecoration = "line-through";
|
||||
}
|
||||
|
||||
if (row?.original?.isSystem) {
|
||||
return (
|
||||
<Badge color="orange.400" style={style}>
|
||||
{intl.formatMessage({ id: "capability.system" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (value?.indexOf("full-admin") !== -1) {
|
||||
return (
|
||||
<Badge color="teal.300" style={style}>
|
||||
{intl.formatMessage({ id: "capability.full-admin" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (value?.length) {
|
||||
const strs: string[] = [];
|
||||
value.map((c: string) => {
|
||||
strs.push(intl.formatMessage({ id: `capability.${c}` }));
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip label={strs.join(", \n")}>
|
||||
<Badge color="cyan.500" style={style}>
|
||||
{intl.formatMessage(
|
||||
{ id: "capability-count" },
|
||||
{ count: value.length },
|
||||
)}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function CertificateStatusFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return (
|
||||
<Badge color={value ? "cyan.500" : "red.400"}>
|
||||
{value
|
||||
? intl.formatMessage({ id: "ready" })
|
||||
: intl.formatMessage({ id: "setup-required" })}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function DisabledFormatter() {
|
||||
const formatCell = ({ value, row }: any) => {
|
||||
if (row?.original?.isDisabled) {
|
||||
return (
|
||||
<Text color="red.500">
|
||||
<Tooltip label={intl.formatMessage({ id: "user.disabled" })}>
|
||||
{value}
|
||||
</Tooltip>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function DNSProviderFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return getNiceDNSProvider(value);
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function DomainsFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
if (value?.length > 0) {
|
||||
return (
|
||||
<>
|
||||
{value.map((dom: string, idx: number) => {
|
||||
return (
|
||||
<Badge key={`domain-${idx}`} color="yellow.400">
|
||||
{dom}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <Badge color="red.400">No domains!</Badge>;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function GravatarFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return <Avatar size="sm" src={value} />;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function HostStatusFormatter() {
|
||||
const formatCell = ({ row }: any) => {
|
||||
if (row.original.isDisabled) {
|
||||
return (
|
||||
<Badge color="red.400">{intl.formatMessage({ id: "disabled" })}</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.original.certificateId) {
|
||||
if (row.original.certificate.status === "provided") {
|
||||
return (
|
||||
<Badge color="green.400">
|
||||
{row.original.sslForced
|
||||
? intl.formatMessage({ id: "https-only" })
|
||||
: intl.formatMessage({ id: "http-https" })}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.original.certificate.status === "error") {
|
||||
return (
|
||||
<Tooltip label={row.original.certificate.errorMessage}>
|
||||
<Badge color="red.400">{intl.formatMessage({ id: "error" })}</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge color="cyan.400">
|
||||
{intl.formatMessage({
|
||||
id: `certificate.${row.original.certificate.status}`,
|
||||
})}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge color="orange.400">
|
||||
{intl.formatMessage({ id: "http-only" })}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function HostTypeFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return intl.formatMessage({ id: `host-type.${value}` });
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function IDFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return <span className="text-muted">{value}</span>;
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
function SecondsFormatter() {
|
||||
const formatCell = ({ value }: any) => {
|
||||
return intl.formatMessage({ id: "seconds" }, { seconds: value });
|
||||
};
|
||||
|
||||
return formatCell;
|
||||
}
|
||||
|
||||
export {
|
||||
ActionsFormatter,
|
||||
BooleanFormatter,
|
||||
CapabilitiesFormatter,
|
||||
CertificateStatusFormatter,
|
||||
DisabledFormatter,
|
||||
DNSProviderFormatter,
|
||||
DomainsFormatter,
|
||||
GravatarFormatter,
|
||||
HostStatusFormatter,
|
||||
HostTypeFormatter,
|
||||
IDFormatter,
|
||||
SecondsFormatter,
|
||||
};
|
70
frontend/src/components/Table/RowActionsMenu.tsx
Normal file
70
frontend/src/components/Table/RowActionsMenu.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
} from "@chakra-ui/react";
|
||||
import { FiMoreVertical } from "react-icons/fi";
|
||||
|
||||
// A row action is a single menu item for the actions column
|
||||
export interface RowAction {
|
||||
title: string;
|
||||
onClick: (e: any, data: any) => any;
|
||||
show?: (data: any) => any;
|
||||
disabled?: (data: any) => any;
|
||||
icon?: any;
|
||||
}
|
||||
|
||||
interface RowActionsProps {
|
||||
/**
|
||||
* Row Data
|
||||
*/
|
||||
data: any;
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: RowAction[];
|
||||
}
|
||||
function RowActionsMenu({ data, actions }: RowActionsProps) {
|
||||
const elms: ReactNode[] = [];
|
||||
actions.map((action) => {
|
||||
if (!action.show || action.show(data)) {
|
||||
const disabled = action.disabled && action.disabled(data);
|
||||
elms.push(
|
||||
<MenuItem
|
||||
key={`action-${action.title}`}
|
||||
icon={action.icon}
|
||||
isDisabled={disabled}
|
||||
onClick={(e: any) => {
|
||||
action.onClick(e, data);
|
||||
}}>
|
||||
{action.title}
|
||||
</MenuItem>,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!elms.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Actions"
|
||||
icon={<FiMoreVertical />}
|
||||
variant="outline"
|
||||
/>
|
||||
<MenuList>{elms}</MenuList>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { RowActionsMenu };
|
57
frontend/src/components/Table/TableHelpers.ts
Normal file
57
frontend/src/components/Table/TableHelpers.ts
Normal file
@ -0,0 +1,57 @@
|
||||
export interface TablePagination {
|
||||
limit: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface TableSortBy {
|
||||
id: string;
|
||||
desc: boolean;
|
||||
}
|
||||
|
||||
export interface TableFilter {
|
||||
id: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
const tableEvents = {
|
||||
FILTERS_CHANGED: "FILTERS_CHANGED",
|
||||
PAGE_CHANGED: "PAGE_CHANGED",
|
||||
PAGE_SIZE_CHANGED: "PAGE_SIZE_CHANGED",
|
||||
TOTAL_COUNT_CHANGED: "TOTAL_COUNT_CHANGED",
|
||||
SORT_CHANGED: "SORT_CHANGED",
|
||||
};
|
||||
|
||||
const tableEventReducer = (state: any, { type, payload }: any) => {
|
||||
switch (type) {
|
||||
case tableEvents.PAGE_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
offset: payload * state.limit,
|
||||
};
|
||||
case tableEvents.PAGE_SIZE_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
limit: payload,
|
||||
};
|
||||
case tableEvents.TOTAL_COUNT_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
total: payload,
|
||||
};
|
||||
case tableEvents.SORT_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
sortBy: payload,
|
||||
};
|
||||
case tableEvents.FILTERS_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
filters: payload,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
export { tableEvents, tableEventReducer };
|
246
frontend/src/components/Table/TableLayout.tsx
Normal file
246
frontend/src/components/Table/TableLayout.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import {
|
||||
ButtonGroup,
|
||||
Center,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Link,
|
||||
Select,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
VStack,
|
||||
} from "@chakra-ui/react";
|
||||
import { TablePagination } from "components";
|
||||
import { intl } from "locale";
|
||||
import {
|
||||
FiChevronsLeft,
|
||||
FiChevronLeft,
|
||||
FiChevronsRight,
|
||||
FiChevronRight,
|
||||
FiChevronDown,
|
||||
FiChevronUp,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
|
||||
export interface TableLayoutProps {
|
||||
pagination: TablePagination;
|
||||
getTableProps: any;
|
||||
getTableBodyProps: any;
|
||||
headerGroups: any;
|
||||
rows: any;
|
||||
prepareRow: any;
|
||||
gotoPage: any;
|
||||
canPreviousPage: any;
|
||||
previousPage: any;
|
||||
canNextPage: any;
|
||||
setPageSize: any;
|
||||
nextPage: any;
|
||||
pageCount: any;
|
||||
pageOptions: any;
|
||||
visibleColumns: any;
|
||||
setAllFilters: any;
|
||||
state: any;
|
||||
}
|
||||
function TableLayout({
|
||||
pagination,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
canPreviousPage,
|
||||
previousPage,
|
||||
canNextPage,
|
||||
setPageSize,
|
||||
nextPage,
|
||||
pageCount,
|
||||
pageOptions,
|
||||
visibleColumns,
|
||||
setAllFilters,
|
||||
state,
|
||||
}: TableLayoutProps) {
|
||||
const currentPage = state.pageIndex + 1;
|
||||
const getPageList = () => {
|
||||
const list = [];
|
||||
for (let x = 0; x < pageOptions.length; x++) {
|
||||
list.push(
|
||||
<option
|
||||
key={`table-pagination-page-${x}`}
|
||||
value={x + 1}
|
||||
selected={currentPage === x + 1}>
|
||||
{x + 1}
|
||||
</option>,
|
||||
);
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
const renderEmpty = (): ReactNode => {
|
||||
return (
|
||||
<Tr>
|
||||
<Td colSpan={visibleColumns.length}>
|
||||
<Center>
|
||||
{state?.filters?.length
|
||||
? intl.formatMessage(
|
||||
{ id: "tables.no-items-with-filters" },
|
||||
{ count: state.filters.length },
|
||||
)
|
||||
: intl.formatMessage({ id: "tables.no-items" })}
|
||||
</Center>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table {...getTableProps()}>
|
||||
<Thead>
|
||||
{headerGroups.map((headerGroup: any) => (
|
||||
<Tr {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
<Th
|
||||
userSelect="none"
|
||||
className={column.className}
|
||||
isNumeric={column.isNumeric}>
|
||||
<Flex alignItems="center">
|
||||
<HStack mx={6} justifyContent="space-between">
|
||||
<Text
|
||||
{...column.getHeaderProps(
|
||||
column.sortable && column.getSortByToggleProps(),
|
||||
)}>
|
||||
{column.render("Header")}
|
||||
</Text>
|
||||
{column.sortable && column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<FiChevronDown />
|
||||
) : (
|
||||
<FiChevronUp />
|
||||
)
|
||||
) : null}
|
||||
{column.Filter ? column.render("Filter") : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody {...getTableBodyProps()}>
|
||||
{rows.length
|
||||
? rows.map((row: any) => {
|
||||
prepareRow(row);
|
||||
return (
|
||||
<Tr {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
<Td
|
||||
{...cell.getCellProps([
|
||||
{
|
||||
className: cell.column.className,
|
||||
},
|
||||
])}>
|
||||
{cell.render("Cell")}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
})
|
||||
: renderEmpty()}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<HStack mx={6} my={4} justifyContent="space-between">
|
||||
<VStack align="left">
|
||||
<Text color="gray.500">
|
||||
{rows.length
|
||||
? intl.formatMessage(
|
||||
{ id: "tables.pagination-counts" },
|
||||
{
|
||||
start: pagination.offset + 1,
|
||||
end: Math.min(
|
||||
pagination.total,
|
||||
pagination.offset + pagination.limit,
|
||||
),
|
||||
total: pagination.total,
|
||||
},
|
||||
)
|
||||
: null}
|
||||
</Text>
|
||||
{state?.filters?.length ? (
|
||||
<Link onClick={() => setAllFilters([])}>
|
||||
<HStack>
|
||||
<FiX display="inline-block" />
|
||||
<Text>
|
||||
{intl.formatMessage(
|
||||
{ id: "tables.clear-all-filters" },
|
||||
{ count: state.filters.length },
|
||||
)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
) : null}
|
||||
</VStack>
|
||||
<nav>
|
||||
<ButtonGroup size="sm" isAttached>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id: "tables.pagination-previous",
|
||||
})}
|
||||
size="sm"
|
||||
icon={<FiChevronsLeft />}
|
||||
isDisabled={!canPreviousPage}
|
||||
onClick={() => gotoPage(0)}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id: "tables.pagination-previous",
|
||||
})}
|
||||
size="sm"
|
||||
icon={<FiChevronLeft />}
|
||||
isDisabled={!canPreviousPage}
|
||||
onClick={() => previousPage()}
|
||||
/>
|
||||
<Select
|
||||
size="sm"
|
||||
variant="filled"
|
||||
borderRadius={0}
|
||||
defaultValue={currentPage}
|
||||
disabled={!canPreviousPage && !canNextPage}
|
||||
aria-label={intl.formatMessage({
|
||||
id: "tables.pagination-select",
|
||||
})}>
|
||||
{getPageList()}
|
||||
</Select>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id: "tables.pagination-next",
|
||||
})}
|
||||
size="sm"
|
||||
icon={<FiChevronRight />}
|
||||
isDisabled={!canNextPage}
|
||||
onClick={() => nextPage()}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label={intl.formatMessage({
|
||||
id: "tables.pagination-next",
|
||||
})}
|
||||
size="sm"
|
||||
icon={<FiChevronsRight />}
|
||||
isDisabled={!canNextPage}
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</nav>
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { TableLayout };
|
142
frontend/src/components/Table/TextFilter.tsx
Normal file
142
frontend/src/components/Table/TextFilter.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverArrow,
|
||||
IconButton,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
Input,
|
||||
Stack,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Select,
|
||||
} from "@chakra-ui/react";
|
||||
import { PrettyButton } from "components";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import { intl } from "locale";
|
||||
import { validateString } from "modules/Validations";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import { FiFilter } from "react-icons/fi";
|
||||
|
||||
function TextFilter({ column: { filterValue, setFilter } }: any) {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure();
|
||||
|
||||
const onSubmit = (values: any, { setSubmitting }: any) => {
|
||||
setFilter(values);
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
setFilter(undefined);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const isFiltered = (): boolean => {
|
||||
return !(typeof filterValue === "undefined" || filterValue === "");
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
placement="right">
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
variant="unstyled"
|
||||
size="sm"
|
||||
color={isFiltered() ? "orange.400" : ""}
|
||||
icon={<FiFilter />}
|
||||
aria-label="Filter"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent p={5}>
|
||||
<FocusLock returnFocus persistentFocus={false}>
|
||||
<PopoverArrow />
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
modifier: filterValue?.modifier || "contains",
|
||||
value: filterValue?.value,
|
||||
} as any
|
||||
}
|
||||
onSubmit={onSubmit}>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<Stack spacing={4}>
|
||||
<Field name="modifier">
|
||||
{({ field, form }: any) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
form.errors.modifier && form.touched.modifier
|
||||
}>
|
||||
<Select
|
||||
{...field}
|
||||
size="sm"
|
||||
id="modifier"
|
||||
defaultValue="contains">
|
||||
<option value="contains">
|
||||
{intl.formatMessage({ id: "filter.contains" })}
|
||||
</option>
|
||||
<option value="equals">
|
||||
{intl.formatMessage({ id: "filter.exactly" })}
|
||||
</option>
|
||||
<option value="starts">
|
||||
{intl.formatMessage({ id: "filter.starts" })}
|
||||
</option>
|
||||
<option value="ends">
|
||||
{intl.formatMessage({ id: "filter.ends" })}
|
||||
</option>
|
||||
</Select>
|
||||
<FormErrorMessage>{form.errors.name}</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="value" validate={validateString(1, 50)}>
|
||||
{({ field, form }: any) => (
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={form.errors.value && form.touched.value}>
|
||||
<Input
|
||||
{...field}
|
||||
size="sm"
|
||||
placeholder={intl.formatMessage({
|
||||
id: "filter.placeholder",
|
||||
})}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FormErrorMessage>{form.errors.value}</FormErrorMessage>
|
||||
</FormControl>
|
||||
)}
|
||||
</Field>
|
||||
<ButtonGroup d="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={clearFilter}
|
||||
isLoading={isSubmitting}>
|
||||
{intl.formatMessage({
|
||||
id: "filter.clear",
|
||||
})}
|
||||
</Button>
|
||||
<PrettyButton size="sm" isLoading={isSubmitting}>
|
||||
{intl.formatMessage({
|
||||
id: "filter.apply",
|
||||
})}
|
||||
</PrettyButton>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FocusLock>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export { TextFilter };
|
5
frontend/src/components/Table/index.ts
Normal file
5
frontend/src/components/Table/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./Formatters";
|
||||
export * from "./RowActionsMenu";
|
||||
export * from "./TableHelpers";
|
||||
export * from "./TableLayout";
|
||||
export * from "./TextFilter";
|
130
frontend/src/components/Table/react-table-config.d.ts
vendored
Normal file
130
frontend/src/components/Table/react-table-config.d.ts
vendored
Normal file
@ -0,0 +1,130 @@
|
||||
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-table#configuration-using-declaration-merging
|
||||
import {
|
||||
UseColumnOrderInstanceProps,
|
||||
UseColumnOrderState,
|
||||
UseExpandedHooks,
|
||||
UseExpandedInstanceProps,
|
||||
UseExpandedOptions,
|
||||
UseExpandedRowProps,
|
||||
UseExpandedState,
|
||||
UseFiltersColumnOptions,
|
||||
UseFiltersColumnProps,
|
||||
UseFiltersInstanceProps,
|
||||
UseFiltersOptions,
|
||||
UseFiltersState,
|
||||
UseGlobalFiltersColumnOptions,
|
||||
UseGlobalFiltersInstanceProps,
|
||||
UseGlobalFiltersOptions,
|
||||
UseGlobalFiltersState,
|
||||
UseGroupByCellProps,
|
||||
UseGroupByColumnOptions,
|
||||
UseGroupByColumnProps,
|
||||
UseGroupByHooks,
|
||||
UseGroupByInstanceProps,
|
||||
UseGroupByOptions,
|
||||
UseGroupByRowProps,
|
||||
UseGroupByState,
|
||||
UsePaginationInstanceProps,
|
||||
UsePaginationOptions,
|
||||
UsePaginationState,
|
||||
UseResizeColumnsColumnOptions,
|
||||
UseResizeColumnsColumnProps,
|
||||
UseResizeColumnsOptions,
|
||||
UseResizeColumnsState,
|
||||
UseRowSelectHooks,
|
||||
UseRowSelectInstanceProps,
|
||||
UseRowSelectOptions,
|
||||
UseRowSelectRowProps,
|
||||
UseRowSelectState,
|
||||
UseRowStateCellProps,
|
||||
UseRowStateInstanceProps,
|
||||
UseRowStateOptions,
|
||||
UseRowStateRowProps,
|
||||
UseRowStateState,
|
||||
UseSortByColumnOptions,
|
||||
UseSortByColumnProps,
|
||||
UseSortByHooks,
|
||||
UseSortByInstanceProps,
|
||||
UseSortByOptions,
|
||||
UseSortByState,
|
||||
} from "react-table";
|
||||
|
||||
declare module "react-table" {
|
||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
||||
|
||||
export interface TableOptions<
|
||||
D extends Record<string, unknown>,
|
||||
> extends UseExpandedOptions<D>,
|
||||
UseFiltersOptions<D>,
|
||||
UseGlobalFiltersOptions<D>,
|
||||
UseGroupByOptions<D>,
|
||||
UsePaginationOptions<D>,
|
||||
UseResizeColumnsOptions<D>,
|
||||
UseRowSelectOptions<D>,
|
||||
UseRowStateOptions<D>,
|
||||
UseSortByOptions<D>,
|
||||
// note that having Record here allows you to add anything to the options, this matches the spirit of the
|
||||
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
|
||||
// feature set, this is a safe default.
|
||||
Record<string, any> {}
|
||||
|
||||
export interface Hooks<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseExpandedHooks<D>,
|
||||
UseGroupByHooks<D>,
|
||||
UseRowSelectHooks<D>,
|
||||
UseSortByHooks<D> {}
|
||||
|
||||
export interface TableInstance<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseColumnOrderInstanceProps<D>,
|
||||
UseExpandedInstanceProps<D>,
|
||||
UseFiltersInstanceProps<D>,
|
||||
UseGlobalFiltersInstanceProps<D>,
|
||||
UseGroupByInstanceProps<D>,
|
||||
UsePaginationInstanceProps<D>,
|
||||
UseRowSelectInstanceProps<D>,
|
||||
UseRowStateInstanceProps<D>,
|
||||
UseSortByInstanceProps<D> {}
|
||||
|
||||
export interface TableState<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseColumnOrderState<D>,
|
||||
UseExpandedState<D>,
|
||||
UseFiltersState<D>,
|
||||
UseGlobalFiltersState<D>,
|
||||
UseGroupByState<D>,
|
||||
UsePaginationState<D>,
|
||||
UseResizeColumnsState<D>,
|
||||
UseRowSelectState<D>,
|
||||
UseRowStateState<D>,
|
||||
UseSortByState<D> {}
|
||||
|
||||
export interface ColumnInterface<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseFiltersColumnOptions<D>,
|
||||
UseGlobalFiltersColumnOptions<D>,
|
||||
UseGroupByColumnOptions<D>,
|
||||
UseResizeColumnsColumnOptions<D>,
|
||||
UseSortByColumnOptions<D> {}
|
||||
|
||||
export interface ColumnInstance<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseFiltersColumnProps<D>,
|
||||
UseGroupByColumnProps<D>,
|
||||
UseResizeColumnsColumnProps<D>,
|
||||
UseSortByColumnProps<D> {}
|
||||
|
||||
export interface Cell<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
// V = any,
|
||||
> extends UseGroupByCellProps<D>,
|
||||
UseRowStateCellProps<D> {}
|
||||
|
||||
export interface Row<
|
||||
D extends Record<string, unknown> = Record<string, unknown>,
|
||||
> extends UseExpandedRowProps<D>,
|
||||
UseGroupByRowProps<D>,
|
||||
UseRowSelectRowProps<D>,
|
||||
UseRowStateRowProps<D> {}
|
||||
}
|
34
frontend/src/components/ThemeSwitcher.tsx
Normal file
34
frontend/src/components/ThemeSwitcher.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import {
|
||||
Icon,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
useColorMode,
|
||||
} from "@chakra-ui/react";
|
||||
import { intl } from "locale";
|
||||
import { FiSun, FiMoon } from "react-icons/fi";
|
||||
|
||||
interface ThemeSwitcherProps {
|
||||
background?: "normal" | "transparent";
|
||||
}
|
||||
function ThemeSwitcher({ background }: ThemeSwitcherProps) {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const additionalProps: Partial<IconButtonProps> = {};
|
||||
if (background === "transparent") {
|
||||
additionalProps["backgroundColor"] = "transparent";
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
{...additionalProps}
|
||||
aria-label={
|
||||
colorMode === "light"
|
||||
? intl.formatMessage({ id: "theme.to-dark" })
|
||||
: intl.formatMessage({ id: "theme.to-light" })
|
||||
}
|
||||
icon={<Icon as={colorMode === "light" ? FiMoon : FiSun} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { ThemeSwitcher };
|
38
frontend/src/components/Unhealthy.tsx
Normal file
38
frontend/src/components/Unhealthy.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Box, Flex, Heading, Text, Stack } from "@chakra-ui/react";
|
||||
import { LocalePicker, ThemeSwitcher } from "components";
|
||||
import { intl } from "locale";
|
||||
import { FaTimes } from "react-icons/fa";
|
||||
|
||||
function Unhealthy() {
|
||||
return (
|
||||
<>
|
||||
<Stack h={10} m={4} justify="end" direction="row">
|
||||
<ThemeSwitcher />
|
||||
<LocalePicker className="text-right" />
|
||||
</Stack>
|
||||
<Box textAlign="center" py={10} px={6}>
|
||||
<Box display="inline-block">
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
bg="red.500"
|
||||
rounded="50px"
|
||||
w="55px"
|
||||
h="55px"
|
||||
textAlign="center">
|
||||
<FaTimes size="30px" color="white" />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Heading as="h2" size="xl" mt={6} mb={2}>
|
||||
{intl.formatMessage({ id: "unhealthy.title" })}
|
||||
</Heading>
|
||||
<Text color="gray.500">
|
||||
{intl.formatMessage({ id: "unhealthy.body" })}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Unhealthy };
|
15
frontend/src/components/index.ts
Normal file
15
frontend/src/components/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export * from "./EmptyList";
|
||||
export * from "./Flag";
|
||||
export * from "./Footer";
|
||||
export * from "./HelpDrawer";
|
||||
export * from "./Loader";
|
||||
export * from "./Loading";
|
||||
export * from "./LocalePicker";
|
||||
export * from "./Navigation";
|
||||
export * from "./Permissions";
|
||||
export * from "./PrettyButton";
|
||||
export * from "./SiteWrapper";
|
||||
export * from "./SpinnerPage";
|
||||
export * from "./Table";
|
||||
export * from "./ThemeSwitcher";
|
||||
export * from "./Unhealthy";
|
79
frontend/src/context/AuthContext.tsx
Normal file
79
frontend/src/context/AuthContext.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { ReactNode, createContext, useContext, useState } from "react";
|
||||
|
||||
import { getToken, refreshToken, TokenResponse } from "api/npm";
|
||||
import AuthStore from "modules/AuthStore";
|
||||
import { useQueryClient } from "react-query";
|
||||
import { useIntervalWhen } from "rooks";
|
||||
|
||||
// Context
|
||||
export interface AuthContextType {
|
||||
authenticated: boolean;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
const initalValue = null;
|
||||
const AuthContext = createContext<AuthContextType | null>(initalValue);
|
||||
|
||||
// Provider
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
tokenRefreshInterval?: number;
|
||||
}
|
||||
function AuthProvider({
|
||||
children,
|
||||
tokenRefreshInterval = 5 * 60 * 1000,
|
||||
}: Props) {
|
||||
const queryClient = useQueryClient();
|
||||
const [authenticated, setAuthenticated] = useState(
|
||||
AuthStore.hasActiveToken(),
|
||||
);
|
||||
|
||||
const handleTokenUpdate = (response: TokenResponse) => {
|
||||
AuthStore.set(response);
|
||||
setAuthenticated(true);
|
||||
};
|
||||
|
||||
const login = async (identity: string, secret: string) => {
|
||||
const type = "password";
|
||||
const response = await getToken({ payload: { type, identity, secret } });
|
||||
handleTokenUpdate(response);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
AuthStore.clear();
|
||||
setAuthenticated(false);
|
||||
queryClient.invalidateQueries("user");
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
const response = await refreshToken();
|
||||
handleTokenUpdate(response);
|
||||
};
|
||||
|
||||
useIntervalWhen(
|
||||
() => {
|
||||
if (authenticated) {
|
||||
refresh();
|
||||
}
|
||||
},
|
||||
tokenRefreshInterval,
|
||||
true,
|
||||
);
|
||||
|
||||
const value = { authenticated, login, logout };
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
function useAuthState() {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error(`useAuthState must be used within a AuthProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { AuthProvider, useAuthState };
|
||||
export default AuthContext;
|
41
frontend/src/context/LocaleContext.tsx
Normal file
41
frontend/src/context/LocaleContext.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
import { getLocale } from "locale";
|
||||
|
||||
// Context
|
||||
export interface LocaleContextType {
|
||||
setLocale: (locale: string) => void;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
const initalValue = null;
|
||||
const LocaleContext = createContext<LocaleContextType | null>(initalValue);
|
||||
|
||||
// Provider
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
function LocaleProvider({ children }: Props) {
|
||||
const [locale, setLocaleValue] = useState(getLocale());
|
||||
|
||||
const setLocale = async (locale: string) => {
|
||||
setLocaleValue(locale);
|
||||
};
|
||||
|
||||
const value = { locale, setLocale };
|
||||
|
||||
return (
|
||||
<LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useLocaleState() {
|
||||
const context = useContext(LocaleContext);
|
||||
if (!context) {
|
||||
throw new Error(`useLocaleState must be used within a LocaleProvider`);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { LocaleProvider, useLocaleState };
|
||||
export default LocaleContext;
|
2
frontend/src/context/index.ts
Normal file
2
frontend/src/context/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./AuthContext";
|
||||
export * from "./LocaleContext";
|
1
frontend/src/declarations.d.ts
vendored
Normal file
1
frontend/src/declarations.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "*.md";
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
frontend/src/hooks/index.ts
Normal file
12
frontend/src/hooks/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export * from "./useCertificateAuthorities";
|
||||
export * from "./useCertificateAuthority";
|
||||
export * from "./useCertificates";
|
||||
export * from "./useDNSProvider";
|
||||
export * from "./useDNSProviders";
|
||||
export * from "./useDNSProvidersAcmesh";
|
||||
export * from "./useHealth";
|
||||
export * from "./useHosts";
|
||||
export * from "./useHostTemplates";
|
||||
export * from "./useSettings";
|
||||
export * from "./useUser";
|
||||
export * from "./useUsers";
|
41
frontend/src/hooks/useCertificateAuthorities.ts
Normal file
41
frontend/src/hooks/useCertificateAuthorities.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
CertificateAuthoritiesResponse,
|
||||
getCertificateAuthorities,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchCertificateAuthorities = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
) => {
|
||||
return getCertificateAuthorities(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useCertificateAuthorities = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<CertificateAuthoritiesResponse, Error>(
|
||||
["certificate-authorities", { offset, limit, sortBy, filters }],
|
||||
() => fetchCertificateAuthorities(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchCertificateAuthorities, useCertificateAuthorities };
|
62
frontend/src/hooks/useCertificateAuthority.ts
Normal file
62
frontend/src/hooks/useCertificateAuthority.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {
|
||||
createCertificateAuthority,
|
||||
getCertificateAuthority,
|
||||
setCertificateAuthority,
|
||||
CertificateAuthority,
|
||||
} from "api/npm";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
|
||||
const fetchCertificateAuthority = (id: any) => {
|
||||
return getCertificateAuthority(id);
|
||||
};
|
||||
|
||||
const useCertificateAuthority = (id: number, options = {}) => {
|
||||
return useQuery<CertificateAuthority, Error>(
|
||||
["certificate-authority", id],
|
||||
() => fetchCertificateAuthority(id),
|
||||
{
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const useSetCertificateAuthority = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(values: CertificateAuthority) => {
|
||||
return values.id
|
||||
? setCertificateAuthority(values.id, values)
|
||||
: createCertificateAuthority(values);
|
||||
},
|
||||
{
|
||||
onMutate: (values) => {
|
||||
const previousObject = queryClient.getQueryData([
|
||||
"certificate-authority",
|
||||
values.id,
|
||||
]);
|
||||
|
||||
queryClient.setQueryData(
|
||||
["certificate-authority", values.id],
|
||||
(old: any) => ({
|
||||
...old,
|
||||
...values,
|
||||
}),
|
||||
);
|
||||
|
||||
return () =>
|
||||
queryClient.setQueryData(
|
||||
["certificate-authority", values.id],
|
||||
previousObject,
|
||||
);
|
||||
},
|
||||
onError: (error, values, rollback: any) => rollback(),
|
||||
onSuccess: async ({ id }: CertificateAuthority) => {
|
||||
queryClient.invalidateQueries(["certificate-authority", id]);
|
||||
queryClient.invalidateQueries("certificate-authorities");
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { useCertificateAuthority, useSetCertificateAuthority };
|
41
frontend/src/hooks/useCertificates.ts
Normal file
41
frontend/src/hooks/useCertificates.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
getCertificates,
|
||||
CertificatesResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchCertificates = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
) => {
|
||||
return getCertificates(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useCertificates = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<CertificatesResponse, Error>(
|
||||
["hosts", { offset, limit, sortBy, filters }],
|
||||
() => fetchCertificates(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchCertificates, useCertificates };
|
56
frontend/src/hooks/useDNSProvider.ts
Normal file
56
frontend/src/hooks/useDNSProvider.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {
|
||||
createDNSProvider,
|
||||
getDNSProvider,
|
||||
setDNSProvider,
|
||||
DNSProvider,
|
||||
} from "api/npm";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
|
||||
const fetchDNSProvider = (id: any) => {
|
||||
return getDNSProvider(id);
|
||||
};
|
||||
|
||||
const useDNSProvider = (id: number, options = {}) => {
|
||||
return useQuery<DNSProvider, Error>(
|
||||
["dns-provider", id],
|
||||
() => fetchDNSProvider(id),
|
||||
{
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const useSetDNSProvider = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(values: DNSProvider) => {
|
||||
return values.id
|
||||
? setDNSProvider(values.id, values)
|
||||
: createDNSProvider(values);
|
||||
},
|
||||
{
|
||||
onMutate: (values) => {
|
||||
const previousObject = queryClient.getQueryData([
|
||||
"dns-provider",
|
||||
values.id,
|
||||
]);
|
||||
|
||||
queryClient.setQueryData(["dns-provider", values.id], (old: any) => ({
|
||||
...old,
|
||||
...values,
|
||||
}));
|
||||
|
||||
return () =>
|
||||
queryClient.setQueryData(["dns-provider", values.id], previousObject);
|
||||
},
|
||||
onError: (error, values, rollback: any) => rollback(),
|
||||
onSuccess: async ({ id }: DNSProvider) => {
|
||||
queryClient.invalidateQueries(["dns-provider", id]);
|
||||
queryClient.invalidateQueries("dns-providers");
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { useDNSProvider, useSetDNSProvider };
|
41
frontend/src/hooks/useDNSProviders.ts
Normal file
41
frontend/src/hooks/useDNSProviders.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
getDNSProviders,
|
||||
DNSProvidersResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchDNSProviders = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
) => {
|
||||
return getDNSProviders(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useDNSProviders = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<DNSProvidersResponse, Error>(
|
||||
["dns-providers", { offset, limit, sortBy, filters }],
|
||||
() => fetchDNSProviders(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { useDNSProviders };
|
16
frontend/src/hooks/useDNSProvidersAcmesh.ts
Normal file
16
frontend/src/hooks/useDNSProvidersAcmesh.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { DNSProvidersAcmesh, getDNSProvidersAcmesh } from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const useDNSProvidersAcmesh = (options = {}) => {
|
||||
return useQuery<DNSProvidersAcmesh[], Error>(
|
||||
["dns-providers-acmesh"],
|
||||
() => getDNSProvidersAcmesh(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { useDNSProvidersAcmesh };
|
16
frontend/src/hooks/useHealth.ts
Normal file
16
frontend/src/hooks/useHealth.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { getHealth, HealthResponse } from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchHealth = () => getHealth();
|
||||
|
||||
const useHealth = (options = {}) => {
|
||||
return useQuery<HealthResponse, Error>("health", fetchHealth, {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 5,
|
||||
refetchInterval: 15 * 1000, // 15 seconds
|
||||
staleTime: 14 * 1000, // 14 seconds
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export { fetchHealth, useHealth };
|
41
frontend/src/hooks/useHostTemplates.ts
Normal file
41
frontend/src/hooks/useHostTemplates.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import {
|
||||
getHostTemplates,
|
||||
HostTemplatesResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchHostTemplates = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
) => {
|
||||
return getHostTemplates(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useHostTemplates = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<HostTemplatesResponse, Error>(
|
||||
["hosts", { offset, limit, sortBy, filters }],
|
||||
() => fetchHostTemplates(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchHostTemplates, useHostTemplates };
|
36
frontend/src/hooks/useHosts.ts
Normal file
36
frontend/src/hooks/useHosts.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {
|
||||
getHosts,
|
||||
HostsResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchHosts = (offset = 0, limit = 10, sortBy?: any, filters?: any) => {
|
||||
return getHosts(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useHosts = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<HostsResponse, Error>(
|
||||
["hosts", { offset, limit, sortBy, filters }],
|
||||
() => fetchHosts(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchHosts, useHosts };
|
36
frontend/src/hooks/useSettings.ts
Normal file
36
frontend/src/hooks/useSettings.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {
|
||||
getSettings,
|
||||
SettingsResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchSettings = (offset = 0, limit = 10, sortBy?: any, filters?: any) => {
|
||||
return getSettings(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useSettings = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<SettingsResponse, Error>(
|
||||
["settings", { offset, limit, sortBy, filters }],
|
||||
() => fetchSettings(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchSettings, useSettings };
|
37
frontend/src/hooks/useUser.ts
Normal file
37
frontend/src/hooks/useUser.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { getUser, setUser, User } from "api/npm";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
|
||||
const fetchUser = (id: any) => {
|
||||
return getUser(id, { expand: "capabilities" });
|
||||
};
|
||||
|
||||
const useUser = (id: string | number, options = {}) => {
|
||||
return useQuery<User, Error>(["user", id], () => fetchUser(id), {
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const useSetUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation((values: User) => setUser(values.id, values), {
|
||||
onMutate: (values) => {
|
||||
const previousObject = queryClient.getQueryData(["user", values.id]);
|
||||
|
||||
queryClient.setQueryData(["user", values.id], (old: any) => ({
|
||||
...old,
|
||||
...values,
|
||||
}));
|
||||
|
||||
return () =>
|
||||
queryClient.setQueryData(["user", values.id], previousObject);
|
||||
},
|
||||
onError: (error, values, rollback: any) => rollback(),
|
||||
onSuccess: async ({ id }: User) => {
|
||||
queryClient.invalidateQueries(["user", id]);
|
||||
queryClient.invalidateQueries("users");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { useUser, useSetUser };
|
36
frontend/src/hooks/useUsers.ts
Normal file
36
frontend/src/hooks/useUsers.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import {
|
||||
getUsers,
|
||||
UsersResponse,
|
||||
tableSortToAPI,
|
||||
tableFiltersToAPI,
|
||||
} from "api/npm";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
const fetchUsers = (offset = 0, limit = 10, sortBy?: any, filters?: any) => {
|
||||
return getUsers(
|
||||
offset,
|
||||
limit,
|
||||
tableSortToAPI(sortBy),
|
||||
tableFiltersToAPI(filters),
|
||||
);
|
||||
};
|
||||
|
||||
const useUsers = (
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortBy?: any,
|
||||
filters?: any,
|
||||
options = {},
|
||||
) => {
|
||||
return useQuery<UsersResponse, Error>(
|
||||
["users", { offset, limit, sortBy, filters }],
|
||||
() => fetchUsers(offset, limit, sortBy, filters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 15 * 1000, // 15 seconds
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export { fetchUsers, useUsers };
|
BIN
frontend/src/img/logo-256.png
Normal file
BIN
frontend/src/img/logo-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
frontend/src/img/logo-text-vertical-grey.png
Normal file
BIN
frontend/src/img/logo-text-vertical-grey.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
59
frontend/src/index.scss
Normal file
59
frontend/src/index.scss
Normal file
@ -0,0 +1,59 @@
|
||||
@import "styles/fonts.scss";
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f5f7fb;
|
||||
color: rgb(73, 80, 87);
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table th.w-80,
|
||||
table td.w-80 {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
/* helpdoc */
|
||||
.helpdoc-body {
|
||||
p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 0 0 0 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-inline-start: var(--chakra-space-6);
|
||||
padding-inline-end: var(--chakra-space-6);
|
||||
padding-top: var(--chakra-space-4);
|
||||
padding-bottom: var(--chakra-space-4);
|
||||
padding-left: 0;
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin: 0;
|
||||
font-size: var(--chakra-fontSizes-xl);
|
||||
font-weight: var(--chakra-fontWeights-semibold);
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-inline-start: var(--chakra-space-4);
|
||||
padding-inline-end: var(--chakra-space-4);
|
||||
padding-top: var(--chakra-space-4);
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
font-size: var(--chakra-fontSizes-lg);
|
||||
font-weight: var(--chakra-fontWeights-semibold);
|
||||
}
|
||||
}
|
28
frontend/src/index.tsx
Normal file
28
frontend/src/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
import { ColorModeScript } from "@chakra-ui/react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import App from "./App";
|
||||
import "./index.scss";
|
||||
import customTheme from "./theme/customTheme";
|
||||
|
||||
declare global {
|
||||
interface Function {
|
||||
Item: React.FC<any>;
|
||||
Link: React.FC<any>;
|
||||
Header: React.FC<any>;
|
||||
Main: React.FC<any>;
|
||||
Options: React.FC<any>;
|
||||
SubTitle: React.FC<any>;
|
||||
Title: React.FC<any>;
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<>
|
||||
<ColorModeScript initialColorMode={customTheme.config.initialColorMode} />
|
||||
<App />
|
||||
</>,
|
||||
document.getElementById("root"),
|
||||
);
|
75
frontend/src/locale/IntlProvider.tsx
Normal file
75
frontend/src/locale/IntlProvider.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { createIntl, createIntlCache } from "react-intl";
|
||||
|
||||
import langDe from "./lang/de.json";
|
||||
import langEn from "./lang/en.json";
|
||||
import langFa from "./lang/fa.json";
|
||||
import langList from "./lang/lang-list.json";
|
||||
|
||||
// first item of each array should be the language code,
|
||||
// not the country code
|
||||
// Remember when adding to this list, also update check-locales.js script
|
||||
const localeOptions = [
|
||||
["en", "en-US"],
|
||||
["de", "de-DE"],
|
||||
["fa", "fa-IR"],
|
||||
];
|
||||
|
||||
const loadMessages = (locale?: string): typeof langList & typeof langEn => {
|
||||
locale = locale || "en";
|
||||
switch (locale.slice(0, 2)) {
|
||||
case "de":
|
||||
return Object.assign({}, langList, langEn, langDe);
|
||||
case "fa":
|
||||
return Object.assign({}, langList, langEn, langFa);
|
||||
default:
|
||||
return Object.assign({}, langList, langEn);
|
||||
}
|
||||
};
|
||||
|
||||
const getFlagCodeForLocale = (locale?: string) => {
|
||||
switch (locale) {
|
||||
case "de-DE":
|
||||
case "de":
|
||||
return "DE";
|
||||
case "fa-IR":
|
||||
case "fa":
|
||||
return "IR";
|
||||
default:
|
||||
return "US";
|
||||
}
|
||||
};
|
||||
|
||||
const getLocale = (short = false) => {
|
||||
let loc = window.localStorage.getItem("locale");
|
||||
if (!loc) {
|
||||
loc = document.documentElement.lang;
|
||||
}
|
||||
if (short) {
|
||||
return loc.slice(0, 2);
|
||||
}
|
||||
return loc;
|
||||
};
|
||||
|
||||
const cache = createIntlCache();
|
||||
|
||||
const initialMessages = loadMessages(getLocale());
|
||||
let intl = createIntl(
|
||||
{ locale: getLocale(), messages: initialMessages },
|
||||
cache,
|
||||
);
|
||||
|
||||
const changeLocale = (locale: string): void => {
|
||||
const messages = loadMessages(locale);
|
||||
intl = createIntl({ locale, messages }, cache);
|
||||
window.localStorage.setItem("locale", locale);
|
||||
document.documentElement.lang = locale;
|
||||
};
|
||||
|
||||
export {
|
||||
localeOptions,
|
||||
getFlagCodeForLocale,
|
||||
getLocale,
|
||||
createIntl,
|
||||
changeLocale,
|
||||
intl,
|
||||
};
|
1
frontend/src/locale/index.ts
Normal file
1
frontend/src/locale/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./IntlProvider";
|
@ -0,0 +1,3 @@
|
||||
# Hilfe zu Zertifizierungsstellen
|
||||
|
||||
TODO
|
1
frontend/src/locale/src/HelpDoc/de/index.ts
Normal file
1
frontend/src/locale/src/HelpDoc/de/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as CertificateAuthorities from "./CertificateAuthorities.md";
|
26
frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md
Normal file
26
frontend/src/locale/src/HelpDoc/en/CertificateAuthorities.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Certificate Authorities Help
|
||||
|
||||
## What is a Certificate Authority?
|
||||
|
||||
A **certificate authority (CA)**, also sometimes referred to as a
|
||||
**certification authority**, is a company or organization that acts to validate
|
||||
the identities of entities (such as websites, email addresses, companies, or
|
||||
individual persons) and bind them to cryptographic keys through the issuance of
|
||||
electronic documents known as digital certificates.
|
||||
|
||||
## Which CA should I use?
|
||||
|
||||
Not all CA's are created equal and you would be fine to use the default,
|
||||
ZeroSSL.
|
||||
|
||||
When using another CA it's worth considering the wildcard support and number of
|
||||
hosts-per-certificate that it supports.
|
||||
|
||||
## Can I use my own custom CA?
|
||||
|
||||
Yes, you can run your own CA software. You would only do this if you have a
|
||||
greater understanding of the SSL ecosystem.
|
||||
|
||||
When requesting SSL Certificates through your custom CA and while they will be
|
||||
successful, browsers will not automatically trust your CA and visiting hosts
|
||||
using certificates issued by that CA will show errors.
|
1
frontend/src/locale/src/HelpDoc/en/index.ts
Normal file
1
frontend/src/locale/src/HelpDoc/en/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as CertificateAuthorities from "./CertificateAuthorities.md";
|
@ -0,0 +1,3 @@
|
||||
# کمک مقامات صدور گواهی
|
||||
|
||||
TODO
|
1
frontend/src/locale/src/HelpDoc/fa/index.ts
Normal file
1
frontend/src/locale/src/HelpDoc/fa/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as CertificateAuthorities from "./CertificateAuthorities.md";
|
17
frontend/src/locale/src/HelpDoc/index.ts
Normal file
17
frontend/src/locale/src/HelpDoc/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import * as de from "./de/index";
|
||||
import * as en from "./en/index";
|
||||
import * as fa from "./fa/index";
|
||||
|
||||
const items: any = { de, en, fa };
|
||||
|
||||
export const getHelpFile = (lang: string, section: string): string => {
|
||||
if (
|
||||
typeof items[lang] !== "undefined" &&
|
||||
typeof items[lang][section] !== "undefined"
|
||||
) {
|
||||
return items[lang][section].default;
|
||||
}
|
||||
throw new Error(`Cannot load help doc for ${lang}-${section}`);
|
||||
};
|
||||
|
||||
export default items;
|
371
frontend/src/locale/src/de.json
Normal file
371
frontend/src/locale/src/de.json
Normal file
@ -0,0 +1,371 @@
|
||||
{
|
||||
"access-lists.title": {
|
||||
"defaultMessage": "Zugriffslisten"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "Bearbeiten"
|
||||
},
|
||||
"action.set-password": {
|
||||
"defaultMessage": "Passwort festlegen"
|
||||
},
|
||||
"audit-log.title": {
|
||||
"defaultMessage": "Audit-Protokoll"
|
||||
},
|
||||
"brand.name": {
|
||||
"defaultMessage": "Nginx Proxy Manager"
|
||||
},
|
||||
"capability-count": {
|
||||
"defaultMessage": "{count} Artikel"
|
||||
},
|
||||
"capability.full-admin": {
|
||||
"defaultMessage": "Vollständiger Administrator"
|
||||
},
|
||||
"capability.hosts.view": {
|
||||
"defaultMessage": "Gastgeber anzeigen"
|
||||
},
|
||||
"capability.system": {
|
||||
"defaultMessage": "System"
|
||||
},
|
||||
"capability.users.manage": {
|
||||
"defaultMessage": "Benutzer verwalten"
|
||||
},
|
||||
"certificate-authorities.title": {
|
||||
"defaultMessage": "Zertifizierungsstellen"
|
||||
},
|
||||
"certificate-authority.acmesh-server": {
|
||||
"defaultMessage": "ACME-Server"
|
||||
},
|
||||
"certificate-authority.ca-bundle": {
|
||||
"defaultMessage": "CA-Zertifikatpaket"
|
||||
},
|
||||
"certificate-authority.create": {
|
||||
"defaultMessage": "Zertifizierungsstelle erstellen"
|
||||
},
|
||||
"certificate-authority.edit": {
|
||||
"defaultMessage": "Zertifizierungsstelle bearbeiten"
|
||||
},
|
||||
"certificate-authority.has-wildcard-support": {
|
||||
"defaultMessage": "Hat Wildcard-Unterstützung"
|
||||
},
|
||||
"certificate-authority.max-domains": {
|
||||
"defaultMessage": "Maximale Domains pro Zertifikat"
|
||||
},
|
||||
"certificate-authority.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Zertifikate"
|
||||
},
|
||||
"change-password": {
|
||||
"defaultMessage": "Passwort ändern"
|
||||
},
|
||||
"column.acmesh-name": {
|
||||
"defaultMessage": "Acme.sh-Plugin"
|
||||
},
|
||||
"column.description": {
|
||||
"defaultMessage": "Beschreibung"
|
||||
},
|
||||
"column.dns-sleep": {
|
||||
"defaultMessage": "DNS-Schlaf"
|
||||
},
|
||||
"column.domain-names": {
|
||||
"defaultMessage": "Domänen"
|
||||
},
|
||||
"column.host-type": {
|
||||
"defaultMessage": "Hosttyp"
|
||||
},
|
||||
"column.id": {
|
||||
"defaultMessage": "ICH WÜRDE"
|
||||
},
|
||||
"column.max-domains": {
|
||||
"defaultMessage": "Domänen pro Zertifikat"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "Status"
|
||||
},
|
||||
"column.type": {
|
||||
"defaultMessage": "Art"
|
||||
},
|
||||
"column.validation-type": {
|
||||
"defaultMessage": "Validierung"
|
||||
},
|
||||
"column.wildcard-support": {
|
||||
"defaultMessage": "Wildcard-Unterstützung"
|
||||
},
|
||||
"create-certificate": {
|
||||
"defaultMessage": "Zertifikat erstellen"
|
||||
},
|
||||
"create-certificate-title": {
|
||||
"defaultMessage": "Es gibt keine Zertifikate"
|
||||
},
|
||||
"create-dns-provider": {
|
||||
"defaultMessage": "Erstellen Sie einen DNS-Anbieter"
|
||||
},
|
||||
"create-dns-provider-title": {
|
||||
"defaultMessage": "Es gibt keine DNS-Anbieter"
|
||||
},
|
||||
"create-hint": {
|
||||
"defaultMessage": "Warum erstellen Sie nicht eine?"
|
||||
},
|
||||
"create-host": {
|
||||
"defaultMessage": "Host erstellen"
|
||||
},
|
||||
"create-host-template": {
|
||||
"defaultMessage": "Hostvorlage erstellen"
|
||||
},
|
||||
"create-host-title": {
|
||||
"defaultMessage": "Es gibt keine Proxy-Hosts"
|
||||
},
|
||||
"dashboard.title": {
|
||||
"defaultMessage": "Armaturenbrett"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "Deaktiviert"
|
||||
},
|
||||
"dns-provider.acmesh-name": {
|
||||
"defaultMessage": "Acme.sh-Name"
|
||||
},
|
||||
"dns-provider.create": {
|
||||
"defaultMessage": "Erstellen Sie einen DNS-Anbieter"
|
||||
},
|
||||
"dns-providers.title": {
|
||||
"defaultMessage": "DNS-Anbieter"
|
||||
},
|
||||
"error": {
|
||||
"defaultMessage": "Fehler"
|
||||
},
|
||||
"error.ca-bundle-does-not-exist": {
|
||||
"defaultMessage": "Datei existiert nicht auf dem Server"
|
||||
},
|
||||
"error.cannot-save-system-users": {
|
||||
"defaultMessage": "Systembenutzer können nicht geändert werden"
|
||||
},
|
||||
"error.current-password-invalid": {
|
||||
"defaultMessage": "Aktuelles Passwort ist ungültig"
|
||||
},
|
||||
"error.database-unavailable": {
|
||||
"defaultMessage": "Datenbank ist nicht verfügbar"
|
||||
},
|
||||
"error.email-already-exists": {
|
||||
"defaultMessage": "Es existiert bereits ein Benutzer mit dieser E-Mail-Adresse"
|
||||
},
|
||||
"error.invalid-login-credentials": {
|
||||
"defaultMessage": "Ungültige Login-Details"
|
||||
},
|
||||
"error.request-failed-validation": {
|
||||
"defaultMessage": "Back-End-Validierung fehlgeschlagen"
|
||||
},
|
||||
"error.user-disabled": {
|
||||
"defaultMessage": "Konto ist deaktiviert"
|
||||
},
|
||||
"filter.apply": {
|
||||
"defaultMessage": "Anwenden"
|
||||
},
|
||||
"filter.clear": {
|
||||
"defaultMessage": "Klar"
|
||||
},
|
||||
"filter.contains": {
|
||||
"defaultMessage": "Enthält"
|
||||
},
|
||||
"filter.ends": {
|
||||
"defaultMessage": "Endet mit"
|
||||
},
|
||||
"filter.exactly": {
|
||||
"defaultMessage": "Exakt"
|
||||
},
|
||||
"filter.placeholder": {
|
||||
"defaultMessage": "Suchbegriff eingeben"
|
||||
},
|
||||
"filter.starts": {
|
||||
"defaultMessage": "Beginnt mit"
|
||||
},
|
||||
"footer.changelog": {
|
||||
"defaultMessage": "Änderungen"
|
||||
},
|
||||
"footer.copyright": {
|
||||
"defaultMessage": "Copyright © {year} jc21.com"
|
||||
},
|
||||
"footer.github": {
|
||||
"defaultMessage": "Github"
|
||||
},
|
||||
"footer.userguide": {
|
||||
"defaultMessage": "Handbuch"
|
||||
},
|
||||
"form.cancel": {
|
||||
"defaultMessage": "Stornieren"
|
||||
},
|
||||
"form.invalid-email": {
|
||||
"defaultMessage": "Ungültige E-Mail-Adresse"
|
||||
},
|
||||
"form.max-int": {
|
||||
"defaultMessage": "Das Maximum ist {count}"
|
||||
},
|
||||
"form.max-length": {
|
||||
"defaultMessage": "Maximum length is {count, plural, one {# character} other {# characters}}"
|
||||
},
|
||||
"form.min-int": {
|
||||
"defaultMessage": "Das Minimum ist {count}"
|
||||
},
|
||||
"form.min-length": {
|
||||
"defaultMessage": "Minimum length is {count, plural, one {# character} other {# characters}}"
|
||||
},
|
||||
"form.required": {
|
||||
"defaultMessage": "Dies ist erforderlich"
|
||||
},
|
||||
"form.save": {
|
||||
"defaultMessage": "Speichern"
|
||||
},
|
||||
"full-access": {
|
||||
"defaultMessage": "Voller Zugriff"
|
||||
},
|
||||
"full-access.description": {
|
||||
"defaultMessage": "Zugriff auf alle Funktionen"
|
||||
},
|
||||
"general-settings.title": {
|
||||
"defaultMessage": "Allgemeine Einstellungen"
|
||||
},
|
||||
"host-templates.title": {
|
||||
"defaultMessage": "Host-Vorlagen"
|
||||
},
|
||||
"hosts.title": {
|
||||
"defaultMessage": "Gastgeber"
|
||||
},
|
||||
"http-https": {
|
||||
"defaultMessage": "HTTP/HTTPS"
|
||||
},
|
||||
"http-only": {
|
||||
"defaultMessage": "Nur HTTP"
|
||||
},
|
||||
"https-only": {
|
||||
"defaultMessage": "Nur HTTPS"
|
||||
},
|
||||
"lets-go": {
|
||||
"defaultMessage": "Lass uns gehen"
|
||||
},
|
||||
"login.login": {
|
||||
"defaultMessage": "Einloggen"
|
||||
},
|
||||
"navigation.close": {
|
||||
"defaultMessage": "Navigation schließen"
|
||||
},
|
||||
"navigation.open": {
|
||||
"defaultMessage": "Navigation öffnen"
|
||||
},
|
||||
"no-access": {
|
||||
"defaultMessage": "Kein Zugang"
|
||||
},
|
||||
"password.confirm": {
|
||||
"defaultMessage": "Bestätige neues Passwort"
|
||||
},
|
||||
"password.current": {
|
||||
"defaultMessage": "Aktuelles Passwort"
|
||||
},
|
||||
"password.new": {
|
||||
"defaultMessage": "Neues Kennwort"
|
||||
},
|
||||
"permissions.title": {
|
||||
"defaultMessage": "Berechtigungen"
|
||||
},
|
||||
"profile.logout": {
|
||||
"defaultMessage": "Ausloggen"
|
||||
},
|
||||
"profile.title": {
|
||||
"defaultMessage": "Profileinstellungen"
|
||||
},
|
||||
"ready": {
|
||||
"defaultMessage": "Bereit"
|
||||
},
|
||||
"restricted-access": {
|
||||
"defaultMessage": "Eingeschränkter Zugang"
|
||||
},
|
||||
"restricted-access.description": {
|
||||
"defaultMessage": "Passen Sie die Berechtigungen für diesen Benutzer an"
|
||||
},
|
||||
"seconds": {
|
||||
"defaultMessage": "{seconds} Sekunden"
|
||||
},
|
||||
"set-password": {
|
||||
"defaultMessage": "Passwort festlegen"
|
||||
},
|
||||
"settings.title": {
|
||||
"defaultMessage": "Einstellungen"
|
||||
},
|
||||
"setup-required": {
|
||||
"defaultMessage": "Setup Required"
|
||||
},
|
||||
"setup.create": {
|
||||
"defaultMessage": "Registrieren"
|
||||
},
|
||||
"setup.title": {
|
||||
"defaultMessage": "Erstelle deinen ersten Account"
|
||||
},
|
||||
"ssl.title": {
|
||||
"defaultMessage": "SSL"
|
||||
},
|
||||
"tables.clear-all-filters": {
|
||||
"defaultMessage": "{count, plural, one {Filter löschen} other {Löschen Sie # Filter}}"
|
||||
},
|
||||
"tables.no-items": {
|
||||
"defaultMessage": "Es sind keine Artikel vorhandenThere are no items"
|
||||
},
|
||||
"tables.no-items-with-filters": {
|
||||
"defaultMessage": "Es gibt keine Elemente, die {count, plural, one {diesem Filter} other {diesen Filtern}} entsprechen"
|
||||
},
|
||||
"tables.pagination-counts": {
|
||||
"defaultMessage": "Showing {start} to {end} of {total, plural, =0 {no items} one {# item} other {# items}}"
|
||||
},
|
||||
"tables.pagination-next": {
|
||||
"defaultMessage": "Nächste Seite"
|
||||
},
|
||||
"tables.pagination-previous": {
|
||||
"defaultMessage": "Vorherige Seite"
|
||||
},
|
||||
"tables.pagination-select": {
|
||||
"defaultMessage": "Wählen Sie eine Seite aus"
|
||||
},
|
||||
"theme.to-dark": {
|
||||
"defaultMessage": "Wechseln Sie zum dunklen Design"
|
||||
},
|
||||
"theme.to-light": {
|
||||
"defaultMessage": "Wechseln Sie zum Lichtdesign"
|
||||
},
|
||||
"unhealthy.body": {
|
||||
"defaultMessage": "Wir werden weiterhin den Zustand überprüfen und hoffen, bald wieder einsatzbereit zu sein!"
|
||||
},
|
||||
"unhealthy.title": {
|
||||
"defaultMessage": "Nginx Proxy Manager ist fehlerhaft"
|
||||
},
|
||||
"user.capabilities": {
|
||||
"defaultMessage": "Fähigkeiten"
|
||||
},
|
||||
"user.create": {
|
||||
"defaultMessage": "Benutzer erstellen"
|
||||
},
|
||||
"user.disabled": {
|
||||
"defaultMessage": "Benutzer ist deaktiviert"
|
||||
},
|
||||
"user.edit": {
|
||||
"defaultMessage": "Benutzer bearbeiten"
|
||||
},
|
||||
"user.email": {
|
||||
"defaultMessage": "E-Mail"
|
||||
},
|
||||
"user.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"user.nickname": {
|
||||
"defaultMessage": "Benutzername"
|
||||
},
|
||||
"user.password": {
|
||||
"defaultMessage": "Passwort"
|
||||
},
|
||||
"users.title": {
|
||||
"defaultMessage": "Benutzer"
|
||||
},
|
||||
"view-only": {
|
||||
"defaultMessage": "Nur anschauen"
|
||||
}
|
||||
}
|
506
frontend/src/locale/src/en.json
Normal file
506
frontend/src/locale/src/en.json
Normal file
@ -0,0 +1,506 @@
|
||||
{
|
||||
"access-lists.title": {
|
||||
"defaultMessage": "Access Lists"
|
||||
},
|
||||
"acmesh.dns_ad": {
|
||||
"defaultMessage": "Alwaysdata"
|
||||
},
|
||||
"acmesh.dns_ali": {
|
||||
"defaultMessage": "Aliyun"
|
||||
},
|
||||
"acmesh.dns_aws": {
|
||||
"defaultMessage": "AWS Route53"
|
||||
},
|
||||
"acmesh.dns_cf": {
|
||||
"defaultMessage": "Cloudflare"
|
||||
},
|
||||
"acmesh.dns_cloudns": {
|
||||
"defaultMessage": "ClouDNS.net"
|
||||
},
|
||||
"acmesh.dns_cx": {
|
||||
"defaultMessage": "CloudXNS"
|
||||
},
|
||||
"acmesh.dns_cyon": {
|
||||
"defaultMessage": "Cyon.ch"
|
||||
},
|
||||
"acmesh.dns_dgon": {
|
||||
"defaultMessage": "DigitalOcean"
|
||||
},
|
||||
"acmesh.dns_dnsimple": {
|
||||
"defaultMessage": "DNSimple"
|
||||
},
|
||||
"acmesh.dns_dp": {
|
||||
"defaultMessage": "DNSPod.cn"
|
||||
},
|
||||
"acmesh.dns_duckdns": {
|
||||
"defaultMessage": "DuckDNS"
|
||||
},
|
||||
"acmesh.dns_dyn": {
|
||||
"defaultMessage": "Dyn"
|
||||
},
|
||||
"acmesh.dns_dynu": {
|
||||
"defaultMessage": "Dynu"
|
||||
},
|
||||
"acmesh.dns_freedns": {
|
||||
"defaultMessage": "FreeDNS"
|
||||
},
|
||||
"acmesh.dns_gandi_livedns": {
|
||||
"defaultMessage": "Gandi LiveDNS"
|
||||
},
|
||||
"acmesh.dns_gd": {
|
||||
"defaultMessage": "GoDaddy"
|
||||
},
|
||||
"acmesh.dns_he": {
|
||||
"defaultMessage": "Hurricane Electric"
|
||||
},
|
||||
"acmesh.dns_infoblox": {
|
||||
"defaultMessage": "Infoblox"
|
||||
},
|
||||
"acmesh.dns_ispconfig": {
|
||||
"defaultMessage": "ISPConfig"
|
||||
},
|
||||
"acmesh.dns_linode_v4": {
|
||||
"defaultMessage": "Linode"
|
||||
},
|
||||
"acmesh.dns_lua": {
|
||||
"defaultMessage": "LuaDNS"
|
||||
},
|
||||
"acmesh.dns_me": {
|
||||
"defaultMessage": "DNSMadeEasy"
|
||||
},
|
||||
"acmesh.dns_namecom": {
|
||||
"defaultMessage": "Name.com"
|
||||
},
|
||||
"acmesh.dns_nsone": {
|
||||
"defaultMessage": "NS1.com"
|
||||
},
|
||||
"acmesh.dns_pdns": {
|
||||
"defaultMessage": "PowerDNS"
|
||||
},
|
||||
"acmesh.dns_unoeuro": {
|
||||
"defaultMessage": "UnoEuro"
|
||||
},
|
||||
"acmesh.dns_vscale": {
|
||||
"defaultMessage": "VSCALE"
|
||||
},
|
||||
"acmesh.dns_yandex": {
|
||||
"defaultMessage": "pdd.yandex.ru"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "Edit"
|
||||
},
|
||||
"action.set-password": {
|
||||
"defaultMessage": "Set Password"
|
||||
},
|
||||
"audit-log.title": {
|
||||
"defaultMessage": "Audit Log"
|
||||
},
|
||||
"brand.name": {
|
||||
"defaultMessage": "Nginx Proxy Manager"
|
||||
},
|
||||
"capability-count": {
|
||||
"defaultMessage": "{count} items"
|
||||
},
|
||||
"capability.access-lists.manage": {
|
||||
"defaultMessage": "Manage Access Lists"
|
||||
},
|
||||
"capability.access-lists.view": {
|
||||
"defaultMessage": "View Access Lists"
|
||||
},
|
||||
"capability.audit-log.view": {
|
||||
"defaultMessage": "View Audit Log"
|
||||
},
|
||||
"capability.certificate-authorities.manage": {
|
||||
"defaultMessage": "Manage Certificate Authorities"
|
||||
},
|
||||
"capability.certificate-authorities.view": {
|
||||
"defaultMessage": "View Certificate Authorities"
|
||||
},
|
||||
"capability.certificates.manage": {
|
||||
"defaultMessage": "Manage Certificates"
|
||||
},
|
||||
"capability.certificates.view": {
|
||||
"defaultMessage": "View Certificates"
|
||||
},
|
||||
"capability.dns-providers.manage": {
|
||||
"defaultMessage": "Manage DNS Providers"
|
||||
},
|
||||
"capability.dns-providers.view": {
|
||||
"defaultMessage": "View DNS Providers"
|
||||
},
|
||||
"capability.full-admin": {
|
||||
"defaultMessage": "Full Admin"
|
||||
},
|
||||
"capability.host-templates.manage": {
|
||||
"defaultMessage": "Manage Host Templates"
|
||||
},
|
||||
"capability.host-templates.view": {
|
||||
"defaultMessage": "View Host Templates"
|
||||
},
|
||||
"capability.hosts.manage": {
|
||||
"defaultMessage": "Manage Hosts"
|
||||
},
|
||||
"capability.hosts.view": {
|
||||
"defaultMessage": "View Hosts"
|
||||
},
|
||||
"capability.settings.manage": {
|
||||
"defaultMessage": "Manage Settings"
|
||||
},
|
||||
"capability.system": {
|
||||
"defaultMessage": "System"
|
||||
},
|
||||
"capability.users.manage": {
|
||||
"defaultMessage": "Manage Users"
|
||||
},
|
||||
"certificate-authorities.title": {
|
||||
"defaultMessage": "Certificate Authorities"
|
||||
},
|
||||
"certificate-authority.acmesh-server": {
|
||||
"defaultMessage": "ACME Server"
|
||||
},
|
||||
"certificate-authority.ca-bundle": {
|
||||
"defaultMessage": "CA Certificate Bundle"
|
||||
},
|
||||
"certificate-authority.create": {
|
||||
"defaultMessage": "Create Certificate Authority"
|
||||
},
|
||||
"certificate-authority.edit": {
|
||||
"defaultMessage": "Edit Certificate Authority"
|
||||
},
|
||||
"certificate-authority.has-wildcard-support": {
|
||||
"defaultMessage": "Has Wildcard Support"
|
||||
},
|
||||
"certificate-authority.max-domains": {
|
||||
"defaultMessage": "Maximum Domains per Certificate"
|
||||
},
|
||||
"certificate-authority.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "Certificates"
|
||||
},
|
||||
"change-password": {
|
||||
"defaultMessage": "Change Password"
|
||||
},
|
||||
"column.acmesh-name": {
|
||||
"defaultMessage": "Acme.sh Plugin"
|
||||
},
|
||||
"column.description": {
|
||||
"defaultMessage": "Description"
|
||||
},
|
||||
"column.dns-sleep": {
|
||||
"defaultMessage": "DNS Sleep"
|
||||
},
|
||||
"column.domain-names": {
|
||||
"defaultMessage": "Domains"
|
||||
},
|
||||
"column.host-type": {
|
||||
"defaultMessage": "Host Type"
|
||||
},
|
||||
"column.id": {
|
||||
"defaultMessage": "ID"
|
||||
},
|
||||
"column.max-domains": {
|
||||
"defaultMessage": "Domains per Cert"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "Status"
|
||||
},
|
||||
"column.type": {
|
||||
"defaultMessage": "Type"
|
||||
},
|
||||
"column.validation-type": {
|
||||
"defaultMessage": "Validation"
|
||||
},
|
||||
"column.wildcard-support": {
|
||||
"defaultMessage": "Wildcard Support"
|
||||
},
|
||||
"create-certificate": {
|
||||
"defaultMessage": "Create Certificate"
|
||||
},
|
||||
"create-certificate-title": {
|
||||
"defaultMessage": "There are no Certificates"
|
||||
},
|
||||
"create-dns-provider": {
|
||||
"defaultMessage": "Create DNS Provider"
|
||||
},
|
||||
"create-dns-provider-title": {
|
||||
"defaultMessage": "There are no DNS Providers"
|
||||
},
|
||||
"create-hint": {
|
||||
"defaultMessage": "Why don't you create one?"
|
||||
},
|
||||
"create-host": {
|
||||
"defaultMessage": "Create Host"
|
||||
},
|
||||
"create-host-template": {
|
||||
"defaultMessage": "Create Host Template"
|
||||
},
|
||||
"create-host-title": {
|
||||
"defaultMessage": "There are no Proxy Hosts"
|
||||
},
|
||||
"dashboard.title": {
|
||||
"defaultMessage": "Dashboard"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "Disabled"
|
||||
},
|
||||
"dns-provider.acmesh-name": {
|
||||
"defaultMessage": "Acme.sh Name"
|
||||
},
|
||||
"dns-provider.create": {
|
||||
"defaultMessage": "Create DNS Provider"
|
||||
},
|
||||
"dns-providers.title": {
|
||||
"defaultMessage": "DNS Providers"
|
||||
},
|
||||
"error": {
|
||||
"defaultMessage": "Error"
|
||||
},
|
||||
"error.ca-bundle-does-not-exist": {
|
||||
"defaultMessage": "File doesn't exist on the server"
|
||||
},
|
||||
"error.cannot-save-system-users": {
|
||||
"defaultMessage": "You cannot modify system users"
|
||||
},
|
||||
"error.current-password-invalid": {
|
||||
"defaultMessage": "Current password is invalid"
|
||||
},
|
||||
"error.database-unavailable": {
|
||||
"defaultMessage": "Database is unavailable"
|
||||
},
|
||||
"error.email-already-exists": {
|
||||
"defaultMessage": "A user already exists with this email address"
|
||||
},
|
||||
"error.invalid-login-credentials": {
|
||||
"defaultMessage": "Invalid login credentials"
|
||||
},
|
||||
"error.request-failed-validation": {
|
||||
"defaultMessage": "Failed backend validation"
|
||||
},
|
||||
"error.user-disabled": {
|
||||
"defaultMessage": "Account is disabled"
|
||||
},
|
||||
"filter.apply": {
|
||||
"defaultMessage": "Apply"
|
||||
},
|
||||
"filter.clear": {
|
||||
"defaultMessage": "Clear"
|
||||
},
|
||||
"filter.contains": {
|
||||
"defaultMessage": "Contains"
|
||||
},
|
||||
"filter.ends": {
|
||||
"defaultMessage": "Ends with"
|
||||
},
|
||||
"filter.exactly": {
|
||||
"defaultMessage": "Exactly"
|
||||
},
|
||||
"filter.placeholder": {
|
||||
"defaultMessage": "Enter search query"
|
||||
},
|
||||
"filter.starts": {
|
||||
"defaultMessage": "Begins with"
|
||||
},
|
||||
"footer.changelog": {
|
||||
"defaultMessage": "Change Log"
|
||||
},
|
||||
"footer.copyright": {
|
||||
"defaultMessage": "Copyright © {year} jc21.com"
|
||||
},
|
||||
"footer.github": {
|
||||
"defaultMessage": "Github"
|
||||
},
|
||||
"footer.userguide": {
|
||||
"defaultMessage": "User Guide"
|
||||
},
|
||||
"form.cancel": {
|
||||
"defaultMessage": "Cancel"
|
||||
},
|
||||
"form.invalid-email": {
|
||||
"defaultMessage": "Invalid email address"
|
||||
},
|
||||
"form.max-int": {
|
||||
"defaultMessage": "Maximum is {count}"
|
||||
},
|
||||
"form.max-length": {
|
||||
"defaultMessage": "Maximum length is {count, plural, one {# character} other {# characters}}"
|
||||
},
|
||||
"form.min-int": {
|
||||
"defaultMessage": "Minimum is {count}"
|
||||
},
|
||||
"form.min-length": {
|
||||
"defaultMessage": "Minimum length is {count, plural, one {# character} other {# characters}}"
|
||||
},
|
||||
"form.required": {
|
||||
"defaultMessage": "This is required"
|
||||
},
|
||||
"form.save": {
|
||||
"defaultMessage": "Save"
|
||||
},
|
||||
"full-access": {
|
||||
"defaultMessage": "Full Access"
|
||||
},
|
||||
"full-access.description": {
|
||||
"defaultMessage": "Access to all functionality"
|
||||
},
|
||||
"general-settings.title": {
|
||||
"defaultMessage": "General Settings"
|
||||
},
|
||||
"host-templates.title": {
|
||||
"defaultMessage": "Host Templates"
|
||||
},
|
||||
"host-type.dead": {
|
||||
"defaultMessage": "404 Host"
|
||||
},
|
||||
"host-type.proxy": {
|
||||
"defaultMessage": "Proxy Host"
|
||||
},
|
||||
"host-type.redirect": {
|
||||
"defaultMessage": "Redirection"
|
||||
},
|
||||
"host-type.stream": {
|
||||
"defaultMessage": "Stream"
|
||||
},
|
||||
"hosts.title": {
|
||||
"defaultMessage": "Hosts"
|
||||
},
|
||||
"http-https": {
|
||||
"defaultMessage": "HTTP/HTTPS"
|
||||
},
|
||||
"http-only": {
|
||||
"defaultMessage": "HTTP Only"
|
||||
},
|
||||
"https-only": {
|
||||
"defaultMessage": "HTTPS Only"
|
||||
},
|
||||
"lets-go": {
|
||||
"defaultMessage": "Let's go"
|
||||
},
|
||||
"login.login": {
|
||||
"defaultMessage": "Sign in"
|
||||
},
|
||||
"navigation.close": {
|
||||
"defaultMessage": "Close navigation"
|
||||
},
|
||||
"navigation.open": {
|
||||
"defaultMessage": "Open navigation"
|
||||
},
|
||||
"no-access": {
|
||||
"defaultMessage": "No Access"
|
||||
},
|
||||
"password.confirm": {
|
||||
"defaultMessage": "Confirm New Password"
|
||||
},
|
||||
"password.current": {
|
||||
"defaultMessage": "Current Password"
|
||||
},
|
||||
"password.new": {
|
||||
"defaultMessage": "New Password"
|
||||
},
|
||||
"permissions.title": {
|
||||
"defaultMessage": "Permissions"
|
||||
},
|
||||
"profile.logout": {
|
||||
"defaultMessage": "Logout"
|
||||
},
|
||||
"profile.title": {
|
||||
"defaultMessage": "Profile"
|
||||
},
|
||||
"ready": {
|
||||
"defaultMessage": "Ready"
|
||||
},
|
||||
"restricted-access": {
|
||||
"defaultMessage": "Restricted Access"
|
||||
},
|
||||
"restricted-access.description": {
|
||||
"defaultMessage": "Fine tune permissions for this user"
|
||||
},
|
||||
"seconds": {
|
||||
"defaultMessage": "{seconds} seconds"
|
||||
},
|
||||
"set-password": {
|
||||
"defaultMessage": "Set Password"
|
||||
},
|
||||
"settings.title": {
|
||||
"defaultMessage": "Settings"
|
||||
},
|
||||
"setup-required": {
|
||||
"defaultMessage": "Setup Required"
|
||||
},
|
||||
"setup.create": {
|
||||
"defaultMessage": "Sign up"
|
||||
},
|
||||
"setup.title": {
|
||||
"defaultMessage": "Create your first Account"
|
||||
},
|
||||
"ssl.title": {
|
||||
"defaultMessage": "SSL"
|
||||
},
|
||||
"tables.clear-all-filters": {
|
||||
"defaultMessage": "Clear {count, plural, one {filter} other {# filters}}"
|
||||
},
|
||||
"tables.no-items": {
|
||||
"defaultMessage": "There are no items"
|
||||
},
|
||||
"tables.no-items-with-filters": {
|
||||
"defaultMessage": "There are no items matching {count, plural, one {this filter} other {these filters}}"
|
||||
},
|
||||
"tables.pagination-counts": {
|
||||
"defaultMessage": "Showing {start} to {end} of {total, plural, =0 {no items} one {# item} other {# items}}"
|
||||
},
|
||||
"tables.pagination-next": {
|
||||
"defaultMessage": "Next page"
|
||||
},
|
||||
"tables.pagination-previous": {
|
||||
"defaultMessage": "Previous page"
|
||||
},
|
||||
"tables.pagination-select": {
|
||||
"defaultMessage": "Select a page"
|
||||
},
|
||||
"theme.to-dark": {
|
||||
"defaultMessage": "Switch to dark theme"
|
||||
},
|
||||
"theme.to-light": {
|
||||
"defaultMessage": "Switch to light theme"
|
||||
},
|
||||
"unhealthy.body": {
|
||||
"defaultMessage": "We'll continue to check the health and hope to be back up and running soon!"
|
||||
},
|
||||
"unhealthy.title": {
|
||||
"defaultMessage": "Nginx Proxy Manager is unhealthy"
|
||||
},
|
||||
"user.capabilities": {
|
||||
"defaultMessage": "Capabilities"
|
||||
},
|
||||
"user.create": {
|
||||
"defaultMessage": "Create User"
|
||||
},
|
||||
"user.disabled": {
|
||||
"defaultMessage": "User is Disabled"
|
||||
},
|
||||
"user.edit": {
|
||||
"defaultMessage": "Edit User"
|
||||
},
|
||||
"user.email": {
|
||||
"defaultMessage": "Email"
|
||||
},
|
||||
"user.name": {
|
||||
"defaultMessage": "Name"
|
||||
},
|
||||
"user.nickname": {
|
||||
"defaultMessage": "Nickname"
|
||||
},
|
||||
"user.password": {
|
||||
"defaultMessage": "Password"
|
||||
},
|
||||
"users.title": {
|
||||
"defaultMessage": "Users"
|
||||
},
|
||||
"view-only": {
|
||||
"defaultMessage": "View Only"
|
||||
}
|
||||
}
|
371
frontend/src/locale/src/fa.json
Normal file
371
frontend/src/locale/src/fa.json
Normal file
@ -0,0 +1,371 @@
|
||||
{
|
||||
"access-lists.title": {
|
||||
"defaultMessage": "دسترسی به لیست ها"
|
||||
},
|
||||
"action.edit": {
|
||||
"defaultMessage": "ویرایش کنید"
|
||||
},
|
||||
"action.set-password": {
|
||||
"defaultMessage": "قراردادن رمز عبور"
|
||||
},
|
||||
"audit-log.title": {
|
||||
"defaultMessage": "گزارش حسابرسی"
|
||||
},
|
||||
"brand.name": {
|
||||
"defaultMessage": "Nginx Proxy Manager"
|
||||
},
|
||||
"capability-count": {
|
||||
"defaultMessage": "{count} مورد"
|
||||
},
|
||||
"capability.full-admin": {
|
||||
"defaultMessage": "ادمین کامل"
|
||||
},
|
||||
"capability.hosts.view": {
|
||||
"defaultMessage": "مشاهده میزبان ها"
|
||||
},
|
||||
"capability.system": {
|
||||
"defaultMessage": "سیستم"
|
||||
},
|
||||
"capability.users.manage": {
|
||||
"defaultMessage": "مدیریت کاربران"
|
||||
},
|
||||
"certificate-authorities.title": {
|
||||
"defaultMessage": "مقامات صدور گواهینامه"
|
||||
},
|
||||
"certificate-authority.acmesh-server": {
|
||||
"defaultMessage": "سرور ACME"
|
||||
},
|
||||
"certificate-authority.ca-bundle": {
|
||||
"defaultMessage": "بسته گواهی CA"
|
||||
},
|
||||
"certificate-authority.create": {
|
||||
"defaultMessage": "ایجاد مرجع صدور گواهینامه"
|
||||
},
|
||||
"certificate-authority.edit": {
|
||||
"defaultMessage": "ویرایش مرجع صدور گواهی"
|
||||
},
|
||||
"certificate-authority.has-wildcard-support": {
|
||||
"defaultMessage": "دارای پشتیبانی Wildcard"
|
||||
},
|
||||
"certificate-authority.max-domains": {
|
||||
"defaultMessage": "حداکثر دامنه در هر گواهی"
|
||||
},
|
||||
"certificate-authority.name": {
|
||||
"defaultMessage": "نام"
|
||||
},
|
||||
"certificates.title": {
|
||||
"defaultMessage": "گواهینامه ها"
|
||||
},
|
||||
"change-password": {
|
||||
"defaultMessage": "رمز عبور را تغییر دهید"
|
||||
},
|
||||
"column.acmesh-name": {
|
||||
"defaultMessage": "پلاگین Acme.sh"
|
||||
},
|
||||
"column.description": {
|
||||
"defaultMessage": "شرح"
|
||||
},
|
||||
"column.dns-sleep": {
|
||||
"defaultMessage": "DNS صبر کنید"
|
||||
},
|
||||
"column.domain-names": {
|
||||
"defaultMessage": "دامنه ها"
|
||||
},
|
||||
"column.host-type": {
|
||||
"defaultMessage": "نوع میزبان"
|
||||
},
|
||||
"column.id": {
|
||||
"defaultMessage": "شناسه"
|
||||
},
|
||||
"column.max-domains": {
|
||||
"defaultMessage": "دامنه در هر گواهی"
|
||||
},
|
||||
"column.name": {
|
||||
"defaultMessage": "نام"
|
||||
},
|
||||
"column.status": {
|
||||
"defaultMessage": "وضعیت"
|
||||
},
|
||||
"column.type": {
|
||||
"defaultMessage": "تایپ کنید"
|
||||
},
|
||||
"column.validation-type": {
|
||||
"defaultMessage": "اعتبار سنجی"
|
||||
},
|
||||
"column.wildcard-support": {
|
||||
"defaultMessage": "پشتیبانی Wildcard"
|
||||
},
|
||||
"create-certificate": {
|
||||
"defaultMessage": "ایجاد گواهی"
|
||||
},
|
||||
"create-certificate-title": {
|
||||
"defaultMessage": "هیچ گواهی وجود ندارد"
|
||||
},
|
||||
"create-dns-provider": {
|
||||
"defaultMessage": "ارائه دهنده DNS ایجاد کنید"
|
||||
},
|
||||
"create-dns-provider-title": {
|
||||
"defaultMessage": "هیچ ارائه دهنده DNS وجود ندارد"
|
||||
},
|
||||
"create-hint": {
|
||||
"defaultMessage": "چرا یکی را ایجاد نمی کنید؟"
|
||||
},
|
||||
"create-host": {
|
||||
"defaultMessage": "هاست ایجاد کنید"
|
||||
},
|
||||
"create-host-template": {
|
||||
"defaultMessage": "قالب هاست ایجاد کنید"
|
||||
},
|
||||
"create-host-title": {
|
||||
"defaultMessage": "هیچ هاست پروکسی وجود ندارد"
|
||||
},
|
||||
"dashboard.title": {
|
||||
"defaultMessage": "داشبورد"
|
||||
},
|
||||
"disabled": {
|
||||
"defaultMessage": "معلول"
|
||||
},
|
||||
"dns-provider.acmesh-name": {
|
||||
"defaultMessage": "نام Acme.sh"
|
||||
},
|
||||
"dns-provider.create": {
|
||||
"defaultMessage": "ارائه دهنده DNS ایجاد کنید"
|
||||
},
|
||||
"dns-providers.title": {
|
||||
"defaultMessage": "ارائه دهندگان DNS"
|
||||
},
|
||||
"error": {
|
||||
"defaultMessage": "خطا"
|
||||
},
|
||||
"error.ca-bundle-does-not-exist": {
|
||||
"defaultMessage": "فایل در سرور وجود ندارد"
|
||||
},
|
||||
"error.cannot-save-system-users": {
|
||||
"defaultMessage": "شما نمی توانید کاربران سیستم را تغییر دهید"
|
||||
},
|
||||
"error.current-password-invalid": {
|
||||
"defaultMessage": "رمز عبور فعلی نامعتبر است"
|
||||
},
|
||||
"error.database-unavailable": {
|
||||
"defaultMessage": "پایگاه داده در دسترس نیست"
|
||||
},
|
||||
"error.email-already-exists": {
|
||||
"defaultMessage": "کاربری از قبل با این آدرس ایمیل وجود دارد"
|
||||
},
|
||||
"error.invalid-login-credentials": {
|
||||
"defaultMessage": "اعتبار ورود نامعتبر است"
|
||||
},
|
||||
"error.request-failed-validation": {
|
||||
"defaultMessage": "اعتبار سنجی پشتیبان ناموفق بود"
|
||||
},
|
||||
"error.user-disabled": {
|
||||
"defaultMessage": "اکانت غیرفعال است"
|
||||
},
|
||||
"filter.apply": {
|
||||
"defaultMessage": "درخواست دادن"
|
||||
},
|
||||
"filter.clear": {
|
||||
"defaultMessage": "پاک کردن"
|
||||
},
|
||||
"filter.contains": {
|
||||
"defaultMessage": "حاوی"
|
||||
},
|
||||
"filter.ends": {
|
||||
"defaultMessage": "به پایان می رسد با"
|
||||
},
|
||||
"filter.exactly": {
|
||||
"defaultMessage": "دقیقا"
|
||||
},
|
||||
"filter.placeholder": {
|
||||
"defaultMessage": "عبارت جستجو را وارد کنید"
|
||||
},
|
||||
"filter.starts": {
|
||||
"defaultMessage": "شروع با"
|
||||
},
|
||||
"footer.changelog": {
|
||||
"defaultMessage": "ورود به سیستم را تغییر دهید"
|
||||
},
|
||||
"footer.copyright": {
|
||||
"defaultMessage": "حق چاپ © حق چاپ © {year} jc21.com"
|
||||
},
|
||||
"footer.github": {
|
||||
"defaultMessage": "Github"
|
||||
},
|
||||
"footer.userguide": {
|
||||
"defaultMessage": "راهنمای کاربر"
|
||||
},
|
||||
"form.cancel": {
|
||||
"defaultMessage": "لغو کنید"
|
||||
},
|
||||
"form.invalid-email": {
|
||||
"defaultMessage": "آدرس ایمیل نامعتبر"
|
||||
},
|
||||
"form.max-int": {
|
||||
"defaultMessage": "حداکثر {count} است"
|
||||
},
|
||||
"form.max-length": {
|
||||
"defaultMessage": "حداکثر طول {count, plural, one {# character} other {# characters}} کاراکتر است"
|
||||
},
|
||||
"form.min-int": {
|
||||
"defaultMessage": "حداقل {count} است"
|
||||
},
|
||||
"form.min-length": {
|
||||
"defaultMessage": "حداقل طول {count, plural, one {# character} other {# characters}} کاراکتر است"
|
||||
},
|
||||
"form.required": {
|
||||
"defaultMessage": "این مورد نیاز است"
|
||||
},
|
||||
"form.save": {
|
||||
"defaultMessage": "صرفه جویی"
|
||||
},
|
||||
"full-access": {
|
||||
"defaultMessage": "دسترسی کامل"
|
||||
},
|
||||
"full-access.description": {
|
||||
"defaultMessage": "دسترسی به تمام قابلیت ها"
|
||||
},
|
||||
"general-settings.title": {
|
||||
"defaultMessage": "تنظیمات عمومی"
|
||||
},
|
||||
"host-templates.title": {
|
||||
"defaultMessage": "قالب های میزبان"
|
||||
},
|
||||
"hosts.title": {
|
||||
"defaultMessage": "میزبان"
|
||||
},
|
||||
"http-https": {
|
||||
"defaultMessage": "HTTP/HTTPS"
|
||||
},
|
||||
"http-only": {
|
||||
"defaultMessage": "فقط HTTP"
|
||||
},
|
||||
"https-only": {
|
||||
"defaultMessage": "فقط HTTPS"
|
||||
},
|
||||
"lets-go": {
|
||||
"defaultMessage": "بیا بریم"
|
||||
},
|
||||
"login.login": {
|
||||
"defaultMessage": "ورود"
|
||||
},
|
||||
"navigation.close": {
|
||||
"defaultMessage": "بستن ناوبری"
|
||||
},
|
||||
"navigation.open": {
|
||||
"defaultMessage": "ناوبری را باز کنید"
|
||||
},
|
||||
"no-access": {
|
||||
"defaultMessage": "هیچ دسترسی"
|
||||
},
|
||||
"password.confirm": {
|
||||
"defaultMessage": "رمز عبور جدید را تأیید کنید"
|
||||
},
|
||||
"password.current": {
|
||||
"defaultMessage": "رمز عبور فعلی"
|
||||
},
|
||||
"password.new": {
|
||||
"defaultMessage": "رمز عبور جدید"
|
||||
},
|
||||
"permissions.title": {
|
||||
"defaultMessage": "مجوزها"
|
||||
},
|
||||
"profile.logout": {
|
||||
"defaultMessage": "خروج"
|
||||
},
|
||||
"profile.title": {
|
||||
"defaultMessage": "تنظیمات نمایه"
|
||||
},
|
||||
"ready": {
|
||||
"defaultMessage": "آماده"
|
||||
},
|
||||
"restricted-access": {
|
||||
"defaultMessage": "دسترسی محدود"
|
||||
},
|
||||
"restricted-access.description": {
|
||||
"defaultMessage": "مجوزهای تنظیم دقیق برای این کاربر"
|
||||
},
|
||||
"seconds": {
|
||||
"defaultMessage": "{seconds} ثانیه"
|
||||
},
|
||||
"set-password": {
|
||||
"defaultMessage": "قراردادن رمز عبور"
|
||||
},
|
||||
"settings.title": {
|
||||
"defaultMessage": "تنظیمات"
|
||||
},
|
||||
"setup-required": {
|
||||
"defaultMessage": "راه اندازی مورد نیاز است"
|
||||
},
|
||||
"setup.create": {
|
||||
"defaultMessage": "ثبت نام"
|
||||
},
|
||||
"setup.title": {
|
||||
"defaultMessage": "اولین حساب خود را ایجاد کنید"
|
||||
},
|
||||
"ssl.title": {
|
||||
"defaultMessage": "SSL"
|
||||
},
|
||||
"tables.clear-all-filters": {
|
||||
"defaultMessage": "{count, plural, one {فیلتر را پاک کنید} other {# فیلتر را پاک کنید}}"
|
||||
},
|
||||
"tables.no-items": {
|
||||
"defaultMessage": "هیچ آیتمی وجود ندارد"
|
||||
},
|
||||
"tables.no-items-with-filters": {
|
||||
"defaultMessage": "{count, plural, one {هیچ موردی مطابق با این فیلتر وجود ندارد} other {هیچ موردی مطابق با این فیلترها وجود ندارد}}"
|
||||
},
|
||||
"tables.pagination-counts": {
|
||||
"defaultMessage": "نمایش {start} تا {end} مورد از {total} مورد"
|
||||
},
|
||||
"tables.pagination-next": {
|
||||
"defaultMessage": "صفحه بعد"
|
||||
},
|
||||
"tables.pagination-previous": {
|
||||
"defaultMessage": "صفحه قبلی"
|
||||
},
|
||||
"tables.pagination-select": {
|
||||
"defaultMessage": "یک صفحه را انتخاب کنید"
|
||||
},
|
||||
"theme.to-dark": {
|
||||
"defaultMessage": "به طرح زمینه تیره بروید"
|
||||
},
|
||||
"theme.to-light": {
|
||||
"defaultMessage": "به طرح زمینه روشن تغییر دهید"
|
||||
},
|
||||
"unhealthy.body": {
|
||||
"defaultMessage": "ما همچنان به بررسی وضعیت سلامتی خود ادامه خواهیم داد و امیدواریم به زودی دوباره راه اندازی شده و کار کنیم!"
|
||||
},
|
||||
"unhealthy.title": {
|
||||
"defaultMessage": "Nginx Proxy Manager ناسالم است"
|
||||
},
|
||||
"user.capabilities": {
|
||||
"defaultMessage": "توانایی ها"
|
||||
},
|
||||
"user.create": {
|
||||
"defaultMessage": "کاربر ایجاد کنید"
|
||||
},
|
||||
"user.disabled": {
|
||||
"defaultMessage": "کاربر غیرفعال است"
|
||||
},
|
||||
"user.edit": {
|
||||
"defaultMessage": "ویرایش کاربر"
|
||||
},
|
||||
"user.email": {
|
||||
"defaultMessage": "پست الکترونیک"
|
||||
},
|
||||
"user.name": {
|
||||
"defaultMessage": "نام"
|
||||
},
|
||||
"user.nickname": {
|
||||
"defaultMessage": "کنیه"
|
||||
},
|
||||
"user.password": {
|
||||
"defaultMessage": "کلمه عبور"
|
||||
},
|
||||
"users.title": {
|
||||
"defaultMessage": "کاربران"
|
||||
},
|
||||
"view-only": {
|
||||
"defaultMessage": "مشاهده فقط"
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user