You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	API lib cleanup, 404 hosts WIP
This commit is contained in:
		| @@ -1,13 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { AccessList } from "./models"; | import type { AccessList } from "./models"; | ||||||
|  |  | ||||||
| export async function createAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | export async function createAccessList(item: AccessList): Promise<AccessList> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/access-lists", | ||||||
| 			url: "/nginx/access-lists", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Certificate } from "./models"; | import type { Certificate } from "./models"; | ||||||
|  |  | ||||||
| export async function createCertificate(item: Certificate, abortController?: AbortController): Promise<Certificate> { | export async function createCertificate(item: Certificate): Promise<Certificate> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/certificates", | ||||||
| 			url: "/nginx/certificates", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { DeadHost } from "./models"; | import type { DeadHost } from "./models"; | ||||||
|  |  | ||||||
| export async function createDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | export async function createDeadHost(item: DeadHost): Promise<DeadHost> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/dead-hosts", | ||||||
| 			url: "/nginx/dead-hosts", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { ProxyHost } from "./models"; | import type { ProxyHost } from "./models"; | ||||||
|  |  | ||||||
| export async function createProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | export async function createProxyHost(item: ProxyHost): Promise<ProxyHost> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/proxy-hosts", | ||||||
| 			url: "/nginx/proxy-hosts", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { RedirectionHost } from "./models"; | import type { RedirectionHost } from "./models"; | ||||||
|  |  | ||||||
| export async function createRedirectionHost( | export async function createRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> { | ||||||
| 	item: RedirectionHost, | 	return await api.post({ | ||||||
| 	abortController?: AbortController, | 		url: "/nginx/redirection-hosts", | ||||||
| ): Promise<RedirectionHost> { | 		// todo: only use whitelist of fields for this data | ||||||
| 	return await api.post( | 		data: item, | ||||||
| 		{ | 	}); | ||||||
| 			url: "/nginx/redirection-hosts", |  | ||||||
| 			// todo: only use whitelist of fields for this data |  | ||||||
| 			data: item, |  | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Stream } from "./models"; | import type { Stream } from "./models"; | ||||||
|  |  | ||||||
| export async function createStream(item: Stream, abortController?: AbortController): Promise<Stream> { | export async function createStream(item: Stream): Promise<Stream> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/streams", | ||||||
| 			url: "/nginx/streams", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,14 +15,11 @@ export interface NewUser { | |||||||
| 	roles?: string[]; | 	roles?: string[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function createUser(item: NewUser, noAuth?: boolean, abortController?: AbortController): Promise<User> { | export async function createUser(item: NewUser, noAuth?: boolean): Promise<User> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/users", | ||||||
| 			url: "/users", | 		// todo: only use whitelist of fields for this data | ||||||
| 			// todo: only use whitelist of fields for this data | 		data: item, | ||||||
| 			data: item, | 		noAuth, | ||||||
| 			noAuth, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteAccessList(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteAccessList(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/access-lists/${id}`, | ||||||
| 			url: `/nginx/access-lists/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteCertificate(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteCertificate(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/certificates/${id}`, | ||||||
| 			url: `/nginx/certificates/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteDeadHost(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteDeadHost(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/dead-hosts/${id}`, | ||||||
| 			url: `/nginx/dead-hosts/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteProxyHost(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteProxyHost(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/proxy-hosts/${id}`, | ||||||
| 			url: `/nginx/proxy-hosts/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteRedirectionHost(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteRedirectionHost(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/redirection-hosts/${id}`, | ||||||
| 			url: `/nginx/redirection-hosts/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteStream(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteStream(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/nginx/streams/${id}`, | ||||||
| 			url: `/nginx/streams/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function deleteUser(id: number, abortController?: AbortController): Promise<boolean> { | export async function deleteUser(id: number): Promise<boolean> { | ||||||
| 	return await api.del( | 	return await api.del({ | ||||||
| 		{ | 		url: `/users/${id}`, | ||||||
| 			url: `/users/${id}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Binary } from "./responseTypes"; | import type { Binary } from "./responseTypes"; | ||||||
|  |  | ||||||
| export async function downloadCertificate(id: number, abortController?: AbortController): Promise<Binary> { | export async function downloadCertificate(id: number): Promise<Binary> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/certificates/${id}/download`, | ||||||
| 			url: `/nginx/certificates/${id}/download`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								frontend/src/api/backend/expansions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/api/backend/expansions.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | export type AccessListExpansion = "owner" | "items" | "clients"; | ||||||
|  | export type AuditLogExpansion = "user"; | ||||||
|  | export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; | ||||||
|  | export type HostExpansion = "owner" | "certificate"; | ||||||
|  | export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; | ||||||
|  | export type UserExpansion = "permissions"; | ||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { AccessListExpansion } from "./expansions"; | ||||||
| import type { AccessList } from "./models"; | import type { AccessList } from "./models"; | ||||||
|  |  | ||||||
| export async function getAccessList(id: number, abortController?: AbortController): Promise<AccessList> { | export async function getAccessList(id: number, expand?: AccessListExpansion[], params = {}): Promise<AccessList> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/access-lists/${id}`, | ||||||
| 			url: `/nginx/access-lists/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { AccessListExpansion } from "./expansions"; | ||||||
| import type { AccessList } from "./models"; | import type { AccessList } from "./models"; | ||||||
|  |  | ||||||
| export type AccessListExpansion = "owner" | "items" | "clients"; |  | ||||||
|  |  | ||||||
| export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> { | export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> { | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/access-lists", | 		url: "/nginx/access-lists", | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { AuditLogExpansion } from "./getAuditLogs"; | import type { AuditLogExpansion } from "./expansions"; | ||||||
| import type { AuditLog } from "./models"; | import type { AuditLog } from "./models"; | ||||||
|  |  | ||||||
| export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> { | export async function getAuditLog(id: number, expand?: AuditLogExpansion[], params = {}): Promise<AuditLog> { | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { AuditLogExpansion } from "./expansions"; | ||||||
| import type { AuditLog } from "./models"; | import type { AuditLog } from "./models"; | ||||||
|  |  | ||||||
| export type AuditLogExpansion = "user"; |  | ||||||
|  |  | ||||||
| export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> { | export async function getAuditLogs(expand?: AuditLogExpansion[], params = {}): Promise<AuditLog[]> { | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/audit-log", | 		url: "/audit-log", | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { CertificateExpansion } from "./expansions"; | ||||||
| import type { Certificate } from "./models"; | import type { Certificate } from "./models"; | ||||||
|  |  | ||||||
| export async function getCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | export async function getCertificate(id: number, expand?: CertificateExpansion[], params = {}): Promise<Certificate> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/certificates/${id}`, | ||||||
| 			url: `/nginx/certificates/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { CertificateExpansion } from "./expansions"; | ||||||
| import type { Certificate } from "./models"; | import type { Certificate } from "./models"; | ||||||
|  |  | ||||||
| export type CertificateExpansion = "owner" | "proxy_hosts" | "redirection_hosts" | "dead_hosts"; |  | ||||||
|  |  | ||||||
| export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> { | export async function getCertificates(expand?: CertificateExpansion[], params = {}): Promise<Certificate[]> { | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/certificates", | 		url: "/nginx/certificates", | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { DeadHost } from "./models"; | import type { DeadHost } from "./models"; | ||||||
|  |  | ||||||
| export async function getDeadHost(id: number, abortController?: AbortController): Promise<DeadHost> { | export async function getDeadHost(id: number, expand?: HostExpansion[], params = {}): Promise<DeadHost> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/dead-hosts/${id}`, | ||||||
| 			url: `/nginx/dead-hosts/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { DeadHost } from "./models"; | import type { DeadHost } from "./models"; | ||||||
|  |  | ||||||
| export type DeadHostExpansion = "owner" | "certificate"; | export async function getDeadHosts(expand?: HostExpansion[], params = {}): Promise<DeadHost[]> { | ||||||
|  |  | ||||||
| export async function getDeadHosts(expand?: DeadHostExpansion[], params = {}): Promise<DeadHost[]> { |  | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/dead-hosts", | 		url: "/nginx/dead-hosts", | ||||||
| 		params: { | 		params: { | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { HealthResponse } from "./responseTypes"; | import type { HealthResponse } from "./responseTypes"; | ||||||
|  |  | ||||||
| export async function getHealth(abortController?: AbortController): Promise<HealthResponse> { | export async function getHealth(): Promise<HealthResponse> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: "/", | ||||||
| 			url: "/", | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function getHostsReport(abortController?: AbortController): Promise<Record<string, number>> { | export async function getHostsReport(): Promise<Record<string, number>> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: "/reports/hosts", | ||||||
| 			url: "/reports/hosts", | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { ProxyHostExpansion } from "./expansions"; | ||||||
| import type { ProxyHost } from "./models"; | import type { ProxyHost } from "./models"; | ||||||
|  |  | ||||||
| export async function getProxyHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | export async function getProxyHost(id: number, expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/proxy-hosts/${id}`, | ||||||
| 			url: `/nginx/proxy-hosts/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { ProxyHostExpansion } from "./expansions"; | ||||||
| import type { ProxyHost } from "./models"; | import type { ProxyHost } from "./models"; | ||||||
|  |  | ||||||
| export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; |  | ||||||
|  |  | ||||||
| export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> { | export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> { | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/proxy-hosts", | 		url: "/nginx/proxy-hosts", | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { ProxyHost } from "./models"; | import type { ProxyHost } from "./models"; | ||||||
|  |  | ||||||
| export async function getRedirectionHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | export async function getRedirectionHost(id: number, expand?: HostExpansion[], params = {}): Promise<ProxyHost> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/redirection-hosts/${id}`, | ||||||
| 			url: `/nginx/redirection-hosts/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { RedirectionHost } from "./models"; | import type { RedirectionHost } from "./models"; | ||||||
|  |  | ||||||
| export type RedirectionHostExpansion = "owner" | "certificate"; | export async function getRedirectionHosts(expand?: HostExpansion[], params = {}): Promise<RedirectionHost[]> { | ||||||
| export async function getRedirectionHosts( |  | ||||||
| 	expand?: RedirectionHostExpansion[], |  | ||||||
| 	params = {}, |  | ||||||
| ): Promise<RedirectionHost[]> { |  | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/redirection-hosts", | 		url: "/nginx/redirection-hosts", | ||||||
| 		params: { | 		params: { | ||||||
|   | |||||||
| @@ -1,11 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Setting } from "./models"; | import type { Setting } from "./models"; | ||||||
|  |  | ||||||
| export async function getSetting(id: string, abortController?: AbortController): Promise<Setting> { | export async function getSetting(id: string, expand?: string[], params = {}): Promise<Setting> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/settings/${id}`, | ||||||
| 			url: `/settings/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { Stream } from "./models"; | import type { Stream } from "./models"; | ||||||
|  |  | ||||||
| export async function getStream(id: number, abortController?: AbortController): Promise<Stream> { | export async function getStream(id: number, expand?: HostExpansion[], params = {}): Promise<Stream> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: `/nginx/streams/${id}`, | ||||||
| 			url: `/nginx/streams/${id}`, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { HostExpansion } from "./expansions"; | ||||||
| import type { Stream } from "./models"; | import type { Stream } from "./models"; | ||||||
|  |  | ||||||
| export type StreamExpansion = "owner" | "certificate"; | export async function getStreams(expand?: HostExpansion[], params = {}): Promise<Stream[]> { | ||||||
|  |  | ||||||
| export async function getStreams(expand?: StreamExpansion[], params = {}): Promise<Stream[]> { |  | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/nginx/streams", | 		url: "/nginx/streams", | ||||||
| 		params: { | 		params: { | ||||||
|   | |||||||
| @@ -1,19 +1,9 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { TokenResponse } from "./responseTypes"; | import type { TokenResponse } from "./responseTypes"; | ||||||
|  |  | ||||||
| interface Options { | export async function getToken(identity: string, secret: string): Promise<TokenResponse> { | ||||||
| 	payload: { | 	return await api.post({ | ||||||
| 		identity: string; | 		url: "/tokens", | ||||||
| 		secret: string; | 		data: { identity, secret }, | ||||||
| 	}; | 	}); | ||||||
| } |  | ||||||
|  |  | ||||||
| export async function getToken({ payload }: Options, abortController?: AbortController): Promise<TokenResponse> { |  | ||||||
| 	return await api.post( |  | ||||||
| 		{ |  | ||||||
| 			url: "/tokens", |  | ||||||
| 			data: payload, |  | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { UserExpansion } from "./expansions"; | ||||||
| import type { User } from "./models"; | import type { User } from "./models"; | ||||||
|  |  | ||||||
| export async function getUser(id: number | string = "me", params = {}): Promise<User> { | export async function getUser(id: number | string = "me", expand?: UserExpansion[], params = {}): Promise<User> { | ||||||
| 	const userId = id ? id : "me"; | 	const userId = id ? id : "me"; | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: `/users/${userId}`, | 		url: `/users/${userId}`, | ||||||
| 		params, | 		params: { | ||||||
|  | 			expand: expand?.join(","), | ||||||
|  | 			...params, | ||||||
|  | 		}, | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  | import type { UserExpansion } from "./expansions"; | ||||||
| import type { User } from "./models"; | import type { User } from "./models"; | ||||||
|  |  | ||||||
| export type UserExpansion = "permissions"; |  | ||||||
|  |  | ||||||
| export async function getUsers(expand?: UserExpansion[], params = {}): Promise<User[]> { | export async function getUsers(expand?: UserExpansion[], params = {}): Promise<User[]> { | ||||||
| 	return await api.get({ | 	return await api.get({ | ||||||
| 		url: "/users", | 		url: "/users", | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ export * from "./deleteRedirectionHost"; | |||||||
| export * from "./deleteStream"; | export * from "./deleteStream"; | ||||||
| export * from "./deleteUser"; | export * from "./deleteUser"; | ||||||
| export * from "./downloadCertificate"; | export * from "./downloadCertificate"; | ||||||
|  | export * from "./expansions"; | ||||||
| export * from "./getAccessList"; | export * from "./getAccessList"; | ||||||
| export * from "./getAccessLists"; | export * from "./getAccessLists"; | ||||||
| export * from "./getAuditLog"; | export * from "./getAuditLog"; | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { TokenResponse } from "./responseTypes"; | import type { TokenResponse } from "./responseTypes"; | ||||||
|  |  | ||||||
| export async function refreshToken(abortController?: AbortController): Promise<TokenResponse> { | export async function refreshToken(): Promise<TokenResponse> { | ||||||
| 	return await api.get( | 	return await api.get({ | ||||||
| 		{ | 		url: "/tokens", | ||||||
| 			url: "/tokens", | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,8 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Certificate } from "./models"; | import type { Certificate } from "./models"; | ||||||
|  |  | ||||||
| export async function renewCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | export async function renewCertificate(id: number): Promise<Certificate> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: `/nginx/certificates/${id}/renew`, | ||||||
| 			url: `/nginx/certificates/${id}/renew`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,17 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { UserPermissions } from "./models"; | import type { UserPermissions } from "./models"; | ||||||
|  |  | ||||||
| export async function setPermissions( | export async function setPermissions(userId: number, data: UserPermissions): Promise<boolean> { | ||||||
| 	userId: number, |  | ||||||
| 	data: UserPermissions, |  | ||||||
| 	abortController?: AbortController, |  | ||||||
| ): Promise<boolean> { |  | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/users/${userId}/permissions`, | ||||||
| 			url: `/users/${userId}/permissions`, | 		data, | ||||||
| 			data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,10 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function testHttpCertificate( | export async function testHttpCertificate(domains: string[]): Promise<Record<string, string>> { | ||||||
| 	domains: string[], | 	return await api.get({ | ||||||
| 	abortController?: AbortController, | 		url: "/nginx/certificates/test-http", | ||||||
| ): Promise<Record<string, string>> { | 		params: { | ||||||
| 	return await api.get( | 			domains: domains.join(","), | ||||||
| 		{ |  | ||||||
| 			url: "/nginx/certificates/test-http", |  | ||||||
| 			params: { |  | ||||||
| 				domains: domains.join(","), |  | ||||||
| 			}, |  | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 	}); | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function toggleDeadHost( | export async function toggleDeadHost(id: number, enabled: boolean): Promise<boolean> { | ||||||
| 	id: number, | 	return await api.post({ | ||||||
| 	enabled: boolean, | 		url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||||
| 	abortController?: AbortController, | 	}); | ||||||
| ): Promise<boolean> { |  | ||||||
| 	return await api.post( |  | ||||||
| 		{ |  | ||||||
| 			url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`, |  | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function toggleProxyHost( | export async function toggleProxyHost(id: number, enabled: boolean): Promise<boolean> { | ||||||
| 	id: number, | 	return await api.post({ | ||||||
| 	enabled: boolean, | 		url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||||
| 	abortController?: AbortController, | 	}); | ||||||
| ): Promise<boolean> { |  | ||||||
| 	return await api.post( |  | ||||||
| 		{ |  | ||||||
| 			url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`, |  | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function toggleRedirectionHost( | export async function toggleRedirectionHost(id: number, enabled: boolean): Promise<boolean> { | ||||||
| 	id: number, | 	return await api.post({ | ||||||
| 	enabled: boolean, | 		url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||||
| 	abortController?: AbortController, | 	}); | ||||||
| ): Promise<boolean> { |  | ||||||
| 	return await api.post( |  | ||||||
| 		{ |  | ||||||
| 			url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`, |  | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
|  |  | ||||||
| export async function toggleStream(id: number, enabled: boolean, abortController?: AbortController): Promise<boolean> { | export async function toggleStream(id: number, enabled: boolean): Promise<boolean> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`, | ||||||
| 			url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { AccessList } from "./models"; | import type { AccessList } from "./models"; | ||||||
|  |  | ||||||
| export async function updateAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | export async function updateAccessList(item: AccessList): Promise<AccessList> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/nginx/access-lists/${id}`, | ||||||
| 			url: `/nginx/access-lists/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,7 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { User } from "./models"; | import type { User } from "./models"; | ||||||
|  |  | ||||||
| export async function updateAuth( | export async function updateAuth(userId: number | "me", newPassword: string, current?: string): Promise<User> { | ||||||
| 	userId: number | "me", |  | ||||||
| 	newPassword: string, |  | ||||||
| 	current?: string, |  | ||||||
| 	abortController?: AbortController, |  | ||||||
| ): Promise<User> { |  | ||||||
| 	const data = { | 	const data = { | ||||||
| 		type: "password", | 		type: "password", | ||||||
| 		current: current, | 		current: current, | ||||||
| @@ -16,11 +11,8 @@ export async function updateAuth( | |||||||
| 		data.current = current; | 		data.current = current; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/users/${userId}/auth`, | ||||||
| 			url: `/users/${userId}/auth`, | 		data, | ||||||
| 			data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { DeadHost } from "./models"; | import type { DeadHost } from "./models"; | ||||||
|  |  | ||||||
| export async function updateDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | export async function updateDeadHost(item: DeadHost): Promise<DeadHost> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/nginx/dead-hosts/${id}`, | ||||||
| 			url: `/nginx/dead-hosts/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { ProxyHost } from "./models"; | import type { ProxyHost } from "./models"; | ||||||
|  |  | ||||||
| export async function updateProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | export async function updateProxyHost(item: ProxyHost): Promise<ProxyHost> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/nginx/proxy-hosts/${id}`, | ||||||
| 			url: `/nginx/proxy-hosts/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,18 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { RedirectionHost } from "./models"; | import type { RedirectionHost } from "./models"; | ||||||
|  |  | ||||||
| export async function updateRedirectionHost( | export async function updateRedirectionHost(item: RedirectionHost): Promise<RedirectionHost> { | ||||||
| 	item: RedirectionHost, |  | ||||||
| 	abortController?: AbortController, |  | ||||||
| ): Promise<RedirectionHost> { |  | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/nginx/redirection-hosts/${id}`, | ||||||
| 			url: `/nginx/redirection-hosts/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Setting } from "./models"; | import type { Setting } from "./models"; | ||||||
|  |  | ||||||
| export async function updateSetting(item: Setting, abortController?: AbortController): Promise<Setting> { | export async function updateSetting(item: Setting): Promise<Setting> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, ...data } = item; | 	const { id, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/settings/${id}`, | ||||||
| 			url: `/settings/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { Stream } from "./models"; | import type { Stream } from "./models"; | ||||||
|  |  | ||||||
| export async function updateStream(item: Stream, abortController?: AbortController): Promise<Stream> { | export async function updateStream(item: Stream): Promise<Stream> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/nginx/streams/${id}`, | ||||||
| 			url: `/nginx/streams/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| import * as api from "./base"; | import * as api from "./base"; | ||||||
| import type { User } from "./models"; | import type { User } from "./models"; | ||||||
|  |  | ||||||
| export async function updateUser(item: User, abortController?: AbortController): Promise<User> { | export async function updateUser(item: User): Promise<User> { | ||||||
| 	// Remove readonly fields | 	// Remove readonly fields | ||||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||||
|  |  | ||||||
| 	return await api.put( | 	return await api.put({ | ||||||
| 		{ | 		url: `/users/${id}`, | ||||||
| 			url: `/users/${id}`, | 		data: data, | ||||||
| 			data: data, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,13 +6,9 @@ export async function uploadCertificate( | |||||||
| 	certificate: string, | 	certificate: string, | ||||||
| 	certificateKey: string, | 	certificateKey: string, | ||||||
| 	intermediateCertificate?: string, | 	intermediateCertificate?: string, | ||||||
| 	abortController?: AbortController, |  | ||||||
| ): Promise<Certificate> { | ): Promise<Certificate> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: `/nginx/certificates/${id}/upload`, | ||||||
| 			url: `/nginx/certificates/${id}/upload`, | 		data: { certificate, certificateKey, intermediateCertificate }, | ||||||
| 			data: { certificate, certificateKey, intermediateCertificate }, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,13 +5,9 @@ export async function validateCertificate( | |||||||
| 	certificate: string, | 	certificate: string, | ||||||
| 	certificateKey: string, | 	certificateKey: string, | ||||||
| 	intermediateCertificate?: string, | 	intermediateCertificate?: string, | ||||||
| 	abortController?: AbortController, |  | ||||||
| ): Promise<ValidatedCertificateResponse> { | ): Promise<ValidatedCertificateResponse> { | ||||||
| 	return await api.post( | 	return await api.post({ | ||||||
| 		{ | 		url: "/nginx/certificates/validate", | ||||||
| 			url: "/nginx/certificates/validate", | 		data: { certificate, certificateKey, intermediateCertificate }, | ||||||
| 			data: { certificate, certificateKey, intermediateCertificate }, | 	}); | ||||||
| 		}, |  | ||||||
| 		abortController, |  | ||||||
| 	); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { intl } from "src/locale"; |  | ||||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||||
| import { Button } from "src/components"; | import { Button } from "src/components"; | ||||||
|  | import { intl } from "src/locale"; | ||||||
|  |  | ||||||
| export function ErrorNotFound() { | export function ErrorNotFound() { | ||||||
| 	const navigate = useNavigate(); | 	const navigate = useNavigate(); | ||||||
| @@ -9,9 +9,7 @@ export function ErrorNotFound() { | |||||||
| 		<div className="container-tight py-4"> | 		<div className="container-tight py-4"> | ||||||
| 			<div className="empty"> | 			<div className="empty"> | ||||||
| 				<p className="empty-title">{intl.formatMessage({ id: "notfound.title" })}</p> | 				<p className="empty-title">{intl.formatMessage({ id: "notfound.title" })}</p> | ||||||
| 				<p className="empty-subtitle text-secondary"> | 				<p className="empty-subtitle text-secondary">{intl.formatMessage({ id: "notfound.text" })}</p> | ||||||
| 					{intl.formatMessage({ id: "notfound.text" })} |  | ||||||
| 				</p> |  | ||||||
| 				<div className="empty-action"> | 				<div className="empty-action"> | ||||||
| 					<Button type="button" size="md" onClick={() => navigate("/")}> | 					<Button type="button" size="md" onClick={() => navigate("/")}> | ||||||
| 						{intl.formatMessage({ id: "notfound.action" })} | 						{intl.formatMessage({ id: "notfound.action" })} | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								frontend/src/components/Form/DomainNamesField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								frontend/src/components/Form/DomainNamesField.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | import { Field, useFormikContext } from "formik"; | ||||||
|  | import type { ActionMeta, MultiValue } from "react-select"; | ||||||
|  | import CreatableSelect from "react-select/creatable"; | ||||||
|  | import { intl } from "src/locale"; | ||||||
|  |  | ||||||
|  | export type SelectOption = { | ||||||
|  | 	label: string; | ||||||
|  | 	value: string; | ||||||
|  | 	color?: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	id?: string; | ||||||
|  | 	maxDomains?: number; | ||||||
|  | 	isWildcardPermitted?: boolean; | ||||||
|  | 	dnsProviderWildcardSupported?: boolean; | ||||||
|  | 	name?: string; | ||||||
|  | 	label?: string; | ||||||
|  | } | ||||||
|  | export function DomainNamesField({ | ||||||
|  | 	name = "domainNames", | ||||||
|  | 	label = "domain-names", | ||||||
|  | 	id = "domainNames", | ||||||
|  | 	maxDomains, | ||||||
|  | 	isWildcardPermitted, | ||||||
|  | 	dnsProviderWildcardSupported, | ||||||
|  | }: Props) { | ||||||
|  | 	const { values, setFieldValue } = useFormikContext(); | ||||||
|  |  | ||||||
|  | 	const getDomainCount = (v: string[] | undefined): number => { | ||||||
|  | 		if (v?.length) { | ||||||
|  | 			return v.length; | ||||||
|  | 		} | ||||||
|  | 		return 0; | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const handleChange = (v: MultiValue<SelectOption>, _actionMeta: ActionMeta<SelectOption>) => { | ||||||
|  | 		const doms = v?.map((i: SelectOption) => { | ||||||
|  | 			return i.value; | ||||||
|  | 		}); | ||||||
|  | 		setFieldValue(name, doms); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const isDomainValid = (d: string): boolean => { | ||||||
|  | 		const dom = d.trim().toLowerCase(); | ||||||
|  | 		const v: any = values; | ||||||
|  |  | ||||||
|  | 		// Deny if the list of domains is hit | ||||||
|  | 		if (maxDomains && getDomainCount(v?.[name]) >= maxDomains) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (dom.length < 3) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Prevent wildcards | ||||||
|  | 		if ((!isWildcardPermitted || !dnsProviderWildcardSupported) && dom.indexOf("*") !== -1) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Prevent duplicate * in domain | ||||||
|  | 		if ((dom.match(/\*/g) || []).length > 1) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Prevent some invalid characters | ||||||
|  | 		if ((dom.match(/(@|,|!|&|\$|#|%|\^|\(|\))/g) || []).length > 0) { | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// This will match *.com type domains, | ||||||
|  | 		return dom.match(/\*\.[^.]+$/m) === null; | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const helperTexts: string[] = []; | ||||||
|  | 	if (maxDomains) { | ||||||
|  | 		helperTexts.push(intl.formatMessage({ id: "domain_names.max" }, { count: maxDomains })); | ||||||
|  | 	} | ||||||
|  | 	if (!isWildcardPermitted) { | ||||||
|  | 		helperTexts.push(intl.formatMessage({ id: "wildcards-not-permitted" })); | ||||||
|  | 	} else if (!dnsProviderWildcardSupported) { | ||||||
|  | 		helperTexts.push(intl.formatMessage({ id: "wildcards-not-supported" })); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ( | ||||||
|  | 		<Field name={name}> | ||||||
|  | 			{({ field, form }: any) => ( | ||||||
|  | 				<div className="mb-3"> | ||||||
|  | 					<label className="form-label" htmlFor={id}> | ||||||
|  | 						{intl.formatMessage({ id: label })} | ||||||
|  | 					</label> | ||||||
|  | 					<CreatableSelect | ||||||
|  | 						name={field.name} | ||||||
|  | 						id={id} | ||||||
|  | 						closeMenuOnSelect={true} | ||||||
|  | 						isClearable={false} | ||||||
|  | 						isValidNewOption={isDomainValid} | ||||||
|  | 						isMulti | ||||||
|  | 						placeholder="Start typing to add domain..." | ||||||
|  | 						onChange={handleChange} | ||||||
|  | 						value={field.value?.map((d: string) => ({ label: d, value: d }))} | ||||||
|  | 					/> | ||||||
|  | 					{form.errors[field.name] ? ( | ||||||
|  | 						<div className="invalid-feedback"> | ||||||
|  | 							{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null} | ||||||
|  | 						</div> | ||||||
|  | 					) : helperTexts.length ? ( | ||||||
|  | 						helperTexts.map((i) => ( | ||||||
|  | 							<div key={i} className="invalid-feedback text-info"> | ||||||
|  | 								{i} | ||||||
|  | 							</div> | ||||||
|  | 						)) | ||||||
|  | 					) : null} | ||||||
|  | 				</div> | ||||||
|  | 			)} | ||||||
|  | 		</Field> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								frontend/src/components/Form/SSLCertificateField.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								frontend/src/components/Form/SSLCertificateField.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | import { IconShield } from "@tabler/icons-react"; | ||||||
|  | import { Field, useFormikContext } from "formik"; | ||||||
|  | import Select, { type ActionMeta, components, type OptionProps } from "react-select"; | ||||||
|  | import type { Certificate } from "src/api/backend"; | ||||||
|  | import { useCertificates } from "src/hooks"; | ||||||
|  | import { DateTimeFormat, intl } from "src/locale"; | ||||||
|  |  | ||||||
|  | interface CertOption { | ||||||
|  | 	readonly value: number | "new"; | ||||||
|  | 	readonly label: string; | ||||||
|  | 	readonly subLabel: string; | ||||||
|  | 	readonly icon: React.ReactNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const Option = (props: OptionProps<CertOption>) => { | ||||||
|  | 	return ( | ||||||
|  | 		<components.Option {...props}> | ||||||
|  | 			<div className="flex-fill"> | ||||||
|  | 				<div className="font-weight-medium"> | ||||||
|  | 					{props.data.icon} <strong>{props.data.label}</strong> | ||||||
|  | 				</div> | ||||||
|  | 				<div className="text-secondary mt-1 ps-3">{props.data.subLabel}</div> | ||||||
|  | 			</div> | ||||||
|  | 		</components.Option> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	id?: string; | ||||||
|  | 	name?: string; | ||||||
|  | 	label?: string; | ||||||
|  | 	required?: boolean; | ||||||
|  | 	allowNew?: boolean; | ||||||
|  | } | ||||||
|  | export function SSLCertificateField({ | ||||||
|  | 	name = "certificateId", | ||||||
|  | 	label = "ssl-certificate", | ||||||
|  | 	id = "certificateId", | ||||||
|  | 	required, | ||||||
|  | 	allowNew, | ||||||
|  | }: Props) { | ||||||
|  | 	const { isLoading, isError, error, data } = useCertificates(); | ||||||
|  |  | ||||||
|  | 	const { setFieldValue } = useFormikContext(); | ||||||
|  |  | ||||||
|  | 	const handleChange = (v: any, _actionMeta: ActionMeta<CertOption>) => { | ||||||
|  | 		setFieldValue(name, v?.value); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const options: CertOption[] = | ||||||
|  | 		data?.map((cert: Certificate) => ({ | ||||||
|  | 			value: cert.id, | ||||||
|  | 			label: cert.niceName, | ||||||
|  | 			subLabel: `${cert.provider === "letsencrypt" ? "Let's Encrypt" : cert.provider} — Expires: ${ | ||||||
|  | 				cert.expiresOn ? DateTimeFormat(cert.expiresOn) : "N/A" | ||||||
|  | 			}`, | ||||||
|  | 			icon: <IconShield size={14} className="text-pink" />, | ||||||
|  | 		})) || []; | ||||||
|  |  | ||||||
|  | 	// Prepend the Add New option | ||||||
|  | 	if (allowNew) { | ||||||
|  | 		options?.unshift({ | ||||||
|  | 			value: "new", | ||||||
|  | 			label: "Request a new HTTP certificate", | ||||||
|  | 			subLabel: "with Let's Encrypt", | ||||||
|  | 			icon: <IconShield size={14} className="text-lime" />, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Prepend the None option | ||||||
|  | 	if (!required) { | ||||||
|  | 		options?.unshift({ | ||||||
|  | 			value: 0, | ||||||
|  | 			label: "None", | ||||||
|  | 			subLabel: "This host will not use HTTPS", | ||||||
|  | 			icon: <IconShield size={14} className="text-red" />, | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return ( | ||||||
|  | 		<Field name={name}> | ||||||
|  | 			{({ field, form }: any) => ( | ||||||
|  | 				<div className="mb-3"> | ||||||
|  | 					<label className="form-label" htmlFor={id}> | ||||||
|  | 						{intl.formatMessage({ id: label })} | ||||||
|  | 					</label> | ||||||
|  | 					{isLoading ? <div className="placeholder placeholder-lg col-12 my-3 placeholder-glow" /> : null} | ||||||
|  | 					{isError ? <div className="invalid-feedback">{`${error}`}</div> : null} | ||||||
|  | 					{!isLoading && !isError ? ( | ||||||
|  | 						<Select | ||||||
|  | 							defaultValue={options[0]} | ||||||
|  | 							options={options} | ||||||
|  | 							components={{ Option }} | ||||||
|  | 							styles={{ | ||||||
|  | 								option: (base) => ({ | ||||||
|  | 									...base, | ||||||
|  | 									height: "100%", | ||||||
|  | 								}), | ||||||
|  | 							}} | ||||||
|  | 							onChange={handleChange} | ||||||
|  | 						/> | ||||||
|  | 					) : null} | ||||||
|  | 					{form.errors[field.name] ? ( | ||||||
|  | 						<div className="invalid-feedback"> | ||||||
|  | 							{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null} | ||||||
|  | 						</div> | ||||||
|  | 					) : null} | ||||||
|  | 				</div> | ||||||
|  | 			)} | ||||||
|  | 		</Field> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								frontend/src/components/Form/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								frontend/src/components/Form/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | export * from "./DomainNamesField"; | ||||||
|  | export * from "./SSLCertificateField"; | ||||||
| @@ -7,11 +7,9 @@ function TableBody<T>(props: TableLayoutProps<T>) { | |||||||
| 	const rows = tableInstance.getRowModel().rows; | 	const rows = tableInstance.getRowModel().rows; | ||||||
|  |  | ||||||
| 	if (rows.length === 0) { | 	if (rows.length === 0) { | ||||||
| 		return emptyState ? ( | 		return ( | ||||||
| 			emptyState |  | ||||||
| 		) : ( |  | ||||||
| 			<tbody className="table-tbody"> | 			<tbody className="table-tbody"> | ||||||
| 				<EmptyRow tableInstance={tableInstance} /> | 				{emptyState ? emptyState : <EmptyRow tableInstance={tableInstance} />} | ||||||
| 			</tbody> | 			</tbody> | ||||||
| 		); | 		); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| export * from "./Button"; | export * from "./Button"; | ||||||
| export * from "./ErrorNotFound"; | export * from "./ErrorNotFound"; | ||||||
| export * from "./Flag"; | export * from "./Flag"; | ||||||
|  | export * from "./Form"; | ||||||
| export * from "./HasPermission"; | export * from "./HasPermission"; | ||||||
| export * from "./Loading"; | export * from "./Loading"; | ||||||
| export * from "./LoadingPage"; | export * from "./LoadingPage"; | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const login = async (identity: string, secret: string) => { | 	const login = async (identity: string, secret: string) => { | ||||||
| 		const response = await getToken({ payload: { identity, secret } }); | 		const response = await getToken(identity, secret); | ||||||
| 		handleTokenUpdate(response); | 		handleTokenUpdate(response); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ export * from "./useAccessLists"; | |||||||
| export * from "./useAuditLog"; | export * from "./useAuditLog"; | ||||||
| export * from "./useAuditLogs"; | export * from "./useAuditLogs"; | ||||||
| export * from "./useCertificates"; | export * from "./useCertificates"; | ||||||
|  | export * from "./useDeadHost"; | ||||||
| export * from "./useDeadHosts"; | export * from "./useDeadHosts"; | ||||||
| export * from "./useHealth"; | export * from "./useHealth"; | ||||||
| export * from "./useHostReport"; | export * from "./useHostReport"; | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								frontend/src/hooks/useDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/hooks/useDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | ||||||
|  | import { createDeadHost, type DeadHost, getDeadHost, updateDeadHost } from "src/api/backend"; | ||||||
|  |  | ||||||
|  | const fetchDeadHost = (id: number | "new") => { | ||||||
|  | 	if (id === "new") { | ||||||
|  | 		return Promise.resolve({ | ||||||
|  | 			id: 0, | ||||||
|  | 			createdOn: "", | ||||||
|  | 			modifiedOn: "", | ||||||
|  | 			ownerUserId: 0, | ||||||
|  | 			domainNames: [], | ||||||
|  | 			certificateId: 0, | ||||||
|  | 			sslForced: false, | ||||||
|  | 			advancedConfig: "", | ||||||
|  | 			meta: {}, | ||||||
|  | 			http2Support: false, | ||||||
|  | 			enabled: true, | ||||||
|  | 			hstsEnabled: false, | ||||||
|  | 			hstsSubdomains: false, | ||||||
|  | 		} as DeadHost); | ||||||
|  | 	} | ||||||
|  | 	return getDeadHost(id, ["owner"]); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const useDeadHost = (id: number | "new", options = {}) => { | ||||||
|  | 	return useQuery<DeadHost, Error>({ | ||||||
|  | 		queryKey: ["dead-host", id], | ||||||
|  | 		queryFn: () => fetchDeadHost(id), | ||||||
|  | 		staleTime: 60 * 1000, // 1 minute | ||||||
|  | 		...options, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const useSetDeadHost = () => { | ||||||
|  | 	const queryClient = useQueryClient(); | ||||||
|  | 	return useMutation({ | ||||||
|  | 		mutationFn: (values: DeadHost) => (values.id ? updateDeadHost(values) : createDeadHost(values)), | ||||||
|  | 		onMutate: (values: DeadHost) => { | ||||||
|  | 			if (!values.id) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			const previousObject = queryClient.getQueryData(["dead-host", values.id]); | ||||||
|  | 			queryClient.setQueryData(["dead-host", values.id], (old: DeadHost) => ({ | ||||||
|  | 				...old, | ||||||
|  | 				...values, | ||||||
|  | 			})); | ||||||
|  | 			return () => queryClient.setQueryData(["dead-host", values.id], previousObject); | ||||||
|  | 		}, | ||||||
|  | 		onError: (_, __, rollback: any) => rollback(), | ||||||
|  | 		onSuccess: async ({ id }: DeadHost) => { | ||||||
|  | 			queryClient.invalidateQueries({ queryKey: ["dead-host", id] }); | ||||||
|  | 			queryClient.invalidateQueries({ queryKey: ["dead-hosts"] }); | ||||||
|  | 		}, | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export { useDeadHost, useSetDeadHost }; | ||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { useQuery } from "@tanstack/react-query"; | import { useQuery } from "@tanstack/react-query"; | ||||||
| import { type DeadHost, type DeadHostExpansion, getDeadHosts } from "src/api/backend"; | import { type DeadHost, getDeadHosts, type HostExpansion } from "src/api/backend"; | ||||||
|  |  | ||||||
| const fetchDeadHosts = (expand?: DeadHostExpansion[]) => { | const fetchDeadHosts = (expand?: HostExpansion[]) => { | ||||||
| 	return getDeadHosts(expand); | 	return getDeadHosts(expand); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useDeadHosts = (expand?: DeadHostExpansion[], options = {}) => { | const useDeadHosts = (expand?: HostExpansion[], options = {}) => { | ||||||
| 	return useQuery<DeadHost[], Error>({ | 	return useQuery<DeadHost[], Error>({ | ||||||
| 		queryKey: ["dead-hosts", { expand }], | 		queryKey: ["dead-hosts", { expand }], | ||||||
| 		queryFn: () => fetchDeadHosts(expand), | 		queryFn: () => fetchDeadHosts(expand), | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { useQuery } from "@tanstack/react-query"; | import { useQuery } from "@tanstack/react-query"; | ||||||
| import { getRedirectionHosts, type RedirectionHost, type RedirectionHostExpansion } from "src/api/backend"; | import { getRedirectionHosts, type HostExpansion, type RedirectionHost } from "src/api/backend"; | ||||||
|  |  | ||||||
| const fetchRedirectionHosts = (expand?: RedirectionHostExpansion[]) => { | const fetchRedirectionHosts = (expand?: HostExpansion[]) => { | ||||||
| 	return getRedirectionHosts(expand); | 	return getRedirectionHosts(expand); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useRedirectionHosts = (expand?: RedirectionHostExpansion[], options = {}) => { | const useRedirectionHosts = (expand?: HostExpansion[], options = {}) => { | ||||||
| 	return useQuery<RedirectionHost[], Error>({ | 	return useQuery<RedirectionHost[], Error>({ | ||||||
| 		queryKey: ["redirection-hosts", { expand }], | 		queryKey: ["redirection-hosts", { expand }], | ||||||
| 		queryFn: () => fetchRedirectionHosts(expand), | 		queryFn: () => fetchRedirectionHosts(expand), | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import { useQuery } from "@tanstack/react-query"; | import { useQuery } from "@tanstack/react-query"; | ||||||
| import { getStreams, type Stream, type StreamExpansion } from "src/api/backend"; | import { getStreams, type HostExpansion, type Stream } from "src/api/backend"; | ||||||
|  |  | ||||||
| const fetchStreams = (expand?: StreamExpansion[]) => { | const fetchStreams = (expand?: HostExpansion[]) => { | ||||||
| 	return getStreams(expand); | 	return getStreams(expand); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useStreams = (expand?: StreamExpansion[], options = {}) => { | const useStreams = (expand?: HostExpansion[], options = {}) => { | ||||||
| 	return useQuery<Stream[], Error>({ | 	return useQuery<Stream[], Error>({ | ||||||
| 		queryKey: ["streams", { expand }], | 		queryKey: ["streams", { expand }], | ||||||
| 		queryFn: () => fetchStreams(expand), | 		queryFn: () => fetchStreams(expand), | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ const fetchUser = (id: number | string) => { | |||||||
| 			avatar: "", | 			avatar: "", | ||||||
| 		} as User); | 		} as User); | ||||||
| 	} | 	} | ||||||
| 	return getUser(id, { expand: "permissions" }); | 	return getUser(id, ["permissions"]); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const useUser = (id: string | number, options = {}) => { | const useUser = (id: string | number, options = {}) => { | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ | |||||||
|   "column.access": "Access", |   "column.access": "Access", | ||||||
|   "column.authorization": "Authorization", |   "column.authorization": "Authorization", | ||||||
|   "column.destination": "Destination", |   "column.destination": "Destination", | ||||||
|  |   "column.details": "Details", | ||||||
|   "column.email": "Email", |   "column.email": "Email", | ||||||
|   "column.event": "Event", |   "column.event": "Event", | ||||||
|   "column.expires": "Expires", |   "column.expires": "Expires", | ||||||
| @@ -40,12 +41,15 @@ | |||||||
|   "column.status": "Status", |   "column.status": "Status", | ||||||
|   "created-on": "Created: {date}", |   "created-on": "Created: {date}", | ||||||
|   "dashboard.title": "Dashboard", |   "dashboard.title": "Dashboard", | ||||||
|  |   "dead-host.edit": "Edit 404 Host", | ||||||
|  |   "dead-host.new": "New 404 Host", | ||||||
|   "dead-hosts.actions-title": "404 Host #{id}", |   "dead-hosts.actions-title": "404 Host #{id}", | ||||||
|   "dead-hosts.add": "Add 404 Host", |   "dead-hosts.add": "Add 404 Host", | ||||||
|   "dead-hosts.count": "{count} 404 Hosts", |   "dead-hosts.count": "{count} 404 Hosts", | ||||||
|   "dead-hosts.empty": "There are no 404 Hosts", |   "dead-hosts.empty": "There are no 404 Hosts", | ||||||
|   "dead-hosts.title": "404 Hosts", |   "dead-hosts.title": "404 Hosts", | ||||||
|   "disabled": "Disabled", |   "disabled": "Disabled", | ||||||
|  |   "domain-names": "Domain Names", | ||||||
|   "email-address": "Email address", |   "email-address": "Email address", | ||||||
|   "empty-subtitle": "Why don't you create one?", |   "empty-subtitle": "Why don't you create one?", | ||||||
|   "error.invalid-auth": "Invalid email or password", |   "error.invalid-auth": "Invalid email or password", | ||||||
| @@ -96,6 +100,7 @@ | |||||||
|   "setup.preamble": "Get started by creating your admin account.", |   "setup.preamble": "Get started by creating your admin account.", | ||||||
|   "setup.title": "Welcome!", |   "setup.title": "Welcome!", | ||||||
|   "sign-in": "Sign in", |   "sign-in": "Sign in", | ||||||
|  |   "ssl-certificate": "SSL Certificate", | ||||||
|   "streams.actions-title": "Stream #{id}", |   "streams.actions-title": "Stream #{id}", | ||||||
|   "streams.add": "Add Stream", |   "streams.add": "Add Stream", | ||||||
|   "streams.count": "{count} Streams", |   "streams.count": "{count} Streams", | ||||||
| @@ -122,5 +127,7 @@ | |||||||
|   "user.switch-light": "Switch to Light mode", |   "user.switch-light": "Switch to Light mode", | ||||||
|   "users.actions-title": "User #{id}", |   "users.actions-title": "User #{id}", | ||||||
|   "users.add": "Add User", |   "users.add": "Add User", | ||||||
|   "users.title": "Users" |   "users.title": "Users", | ||||||
|  |   "wildcards-not-permitted": "Wildcards not permitted for this type", | ||||||
|  |   "wildcards-not-supported": "Wildcards not supported for this CA" | ||||||
| } | } | ||||||
| @@ -77,6 +77,9 @@ | |||||||
| 	"column.destination": { | 	"column.destination": { | ||||||
| 		"defaultMessage": "Destination" | 		"defaultMessage": "Destination" | ||||||
| 	}, | 	}, | ||||||
|  | 	"column.details": { | ||||||
|  | 		"defaultMessage": "Details" | ||||||
|  | 	}, | ||||||
| 	"column.email": { | 	"column.email": { | ||||||
| 		"defaultMessage": "Email" | 		"defaultMessage": "Email" | ||||||
| 	}, | 	}, | ||||||
| @@ -131,15 +134,24 @@ | |||||||
| 	"dead-hosts.count": { | 	"dead-hosts.count": { | ||||||
| 		"defaultMessage": "{count} 404 Hosts" | 		"defaultMessage": "{count} 404 Hosts" | ||||||
| 	}, | 	}, | ||||||
|  | 	"dead-host.edit": { | ||||||
|  | 		"defaultMessage": "Edit 404 Host" | ||||||
|  | 	}, | ||||||
| 	"dead-hosts.empty": { | 	"dead-hosts.empty": { | ||||||
| 		"defaultMessage": "There are no 404 Hosts" | 		"defaultMessage": "There are no 404 Hosts" | ||||||
| 	}, | 	}, | ||||||
|  | 	"dead-host.new": { | ||||||
|  | 		"defaultMessage": "New 404 Host" | ||||||
|  | 	}, | ||||||
| 	"dead-hosts.title": { | 	"dead-hosts.title": { | ||||||
| 		"defaultMessage": "404 Hosts" | 		"defaultMessage": "404 Hosts" | ||||||
| 	}, | 	}, | ||||||
| 	"disabled": { | 	"disabled": { | ||||||
| 		"defaultMessage": "Disabled" | 		"defaultMessage": "Disabled" | ||||||
| 	}, | 	}, | ||||||
|  | 	"domain-names": { | ||||||
|  | 		"defaultMessage": "Domain Names" | ||||||
|  | 	}, | ||||||
| 	"email-address": { | 	"email-address": { | ||||||
| 		"defaultMessage": "Email address" | 		"defaultMessage": "Email address" | ||||||
| 	}, | 	}, | ||||||
| @@ -290,6 +302,9 @@ | |||||||
| 	"sign-in": { | 	"sign-in": { | ||||||
| 		"defaultMessage": "Sign in" | 		"defaultMessage": "Sign in" | ||||||
| 	}, | 	}, | ||||||
|  | 	"ssl-certificate": { | ||||||
|  | 		"defaultMessage": "SSL Certificate" | ||||||
|  | 	}, | ||||||
| 	"streams.actions-title": { | 	"streams.actions-title": { | ||||||
| 		"defaultMessage": "Stream #{id}" | 		"defaultMessage": "Stream #{id}" | ||||||
| 	}, | 	}, | ||||||
| @@ -370,5 +385,11 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"users.title": { | 	"users.title": { | ||||||
| 		"defaultMessage": "Users" | 		"defaultMessage": "Users" | ||||||
|  | 	}, | ||||||
|  | 	"wildcards-not-permitted": { | ||||||
|  | 		"defaultMessage": "Wildcards not permitted for this type" | ||||||
|  | 	}, | ||||||
|  | 	"wildcards-not-supported": { | ||||||
|  | 		"defaultMessage": "Wildcards not supported for this CA" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										285
									
								
								frontend/src/modals/DeadHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								frontend/src/modals/DeadHostModal.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | |||||||
|  | import { IconSettings } from "@tabler/icons-react"; | ||||||
|  | import { Form, Formik } from "formik"; | ||||||
|  | import { useState } from "react"; | ||||||
|  | import { Alert } from "react-bootstrap"; | ||||||
|  | import Modal from "react-bootstrap/Modal"; | ||||||
|  | import { Button, DomainNamesField, Loading, SSLCertificateField } from "src/components"; | ||||||
|  | import { useDeadHost } from "src/hooks"; | ||||||
|  | import { intl } from "src/locale"; | ||||||
|  |  | ||||||
|  | interface Props { | ||||||
|  | 	id: number | "new"; | ||||||
|  | 	onClose: () => void; | ||||||
|  | } | ||||||
|  | export function DeadHostModal({ id, onClose }: Props) { | ||||||
|  | 	const { data, isLoading, error } = useDeadHost(id); | ||||||
|  | 	// const { mutate: setDeadHost } = useSetDeadHost(); | ||||||
|  | 	const [errorMsg, setErrorMsg] = useState<string | null>(null); | ||||||
|  |  | ||||||
|  | 	const onSubmit = async (values: any, { setSubmitting }: any) => { | ||||||
|  | 		setSubmitting(true); | ||||||
|  | 		setErrorMsg(null); | ||||||
|  | 		console.log("SUBMIT:", values); | ||||||
|  | 		setSubmitting(false); | ||||||
|  | 		// const { ...payload } = { | ||||||
|  | 		// 	id: id === "new" ? undefined : id, | ||||||
|  | 		// 	roles: [], | ||||||
|  | 		// 	...values, | ||||||
|  | 		// }; | ||||||
|  |  | ||||||
|  | 		// setDeadHost(payload, { | ||||||
|  | 		// 	onError: (err: any) => setErrorMsg(err.message), | ||||||
|  | 		// 	onSuccess: () => { | ||||||
|  | 		// 		showSuccess(intl.formatMessage({ id: "notification.dead-host-saved" })); | ||||||
|  | 		// 		onClose(); | ||||||
|  | 		// 	}, | ||||||
|  | 		// 	onSettled: () => setSubmitting(false), | ||||||
|  | 		// }); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	return ( | ||||||
|  | 		<Modal show onHide={onClose} animation={false}> | ||||||
|  | 			{!isLoading && error && ( | ||||||
|  | 				<Alert variant="danger" className="m-3"> | ||||||
|  | 					{error?.message || "Unknown error"} | ||||||
|  | 				</Alert> | ||||||
|  | 			)} | ||||||
|  | 			{isLoading && <Loading noLogo />} | ||||||
|  | 			{!isLoading && data && ( | ||||||
|  | 				<Formik | ||||||
|  | 					initialValues={ | ||||||
|  | 						{ | ||||||
|  | 							domainNames: data?.domainNames, | ||||||
|  | 							certificateId: data?.certificateId, | ||||||
|  | 							sslForced: data?.sslForced, | ||||||
|  | 							advancedConfig: data?.advancedConfig, | ||||||
|  | 							http2Support: data?.http2Support, | ||||||
|  | 							hstsEnabled: data?.hstsEnabled, | ||||||
|  | 							hstsSubdomains: data?.hstsSubdomains, | ||||||
|  | 						} as any | ||||||
|  | 					} | ||||||
|  | 					onSubmit={onSubmit} | ||||||
|  | 				> | ||||||
|  | 					{({ isSubmitting }) => ( | ||||||
|  | 						<Form> | ||||||
|  | 							<Modal.Header closeButton> | ||||||
|  | 								<Modal.Title> | ||||||
|  | 									{intl.formatMessage({ id: data?.id ? "dead-host.edit" : "dead-host.new" })} | ||||||
|  | 								</Modal.Title> | ||||||
|  | 							</Modal.Header> | ||||||
|  | 							<Modal.Body className="p-0"> | ||||||
|  | 								<Alert variant="danger" show={!!errorMsg} onClose={() => setErrorMsg(null)} dismissible> | ||||||
|  | 									{errorMsg} | ||||||
|  | 								</Alert> | ||||||
|  |  | ||||||
|  | 								<div className="card m-0 border-0"> | ||||||
|  | 									<div className="card-header"> | ||||||
|  | 										<ul className="nav nav-tabs card-header-tabs" data-bs-toggle="tabs"> | ||||||
|  | 											<li className="nav-item" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-details" | ||||||
|  | 													className="nav-link active" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="true" | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													{intl.formatMessage({ id: "column.details" })} | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 											<li className="nav-item" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-ssl" | ||||||
|  | 													className="nav-link" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="false" | ||||||
|  | 													tabIndex={-1} | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													{intl.formatMessage({ id: "column.ssl" })} | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 											<li className="nav-item ms-auto" role="presentation"> | ||||||
|  | 												<a | ||||||
|  | 													href="#tab-advanced" | ||||||
|  | 													className="nav-link" | ||||||
|  | 													title="Settings" | ||||||
|  | 													data-bs-toggle="tab" | ||||||
|  | 													aria-selected="false" | ||||||
|  | 													tabIndex={-1} | ||||||
|  | 													role="tab" | ||||||
|  | 												> | ||||||
|  | 													<IconSettings size={20} /> | ||||||
|  | 												</a> | ||||||
|  | 											</li> | ||||||
|  | 										</ul> | ||||||
|  | 									</div> | ||||||
|  | 									<div className="card-body"> | ||||||
|  | 										<div className="tab-content"> | ||||||
|  | 											<div className="tab-pane active show" id="tab-details" role="tabpanel"> | ||||||
|  | 												<DomainNamesField isWildcardPermitted /> | ||||||
|  | 											</div> | ||||||
|  | 											<div className="tab-pane" id="tab-ssl" role="tabpanel"> | ||||||
|  | 												<SSLCertificateField | ||||||
|  | 													name="certificateId" | ||||||
|  | 													label="ssl-certificate" | ||||||
|  | 													allowNew | ||||||
|  | 												/> | ||||||
|  | 											</div> | ||||||
|  | 											<div className="tab-pane" id="tab-advanced" role="tabpanel"> | ||||||
|  | 												<h4>Advanced</h4> | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 									</div> | ||||||
|  | 								</div> | ||||||
|  |  | ||||||
|  | 								{/* <div className="row"> | ||||||
|  | 									<div className="col-lg-6"> | ||||||
|  | 										<div className="mb-3"> | ||||||
|  | 											<Field name="name" validate={validateString(1, 50)}> | ||||||
|  | 												{({ field, form }: any) => ( | ||||||
|  | 													<div className="form-floating mb-3"> | ||||||
|  | 														<input | ||||||
|  | 															id="name" | ||||||
|  | 															className={`form-control ${form.errors.name && form.touched.name ? "is-invalid" : ""}`} | ||||||
|  | 															placeholder={intl.formatMessage({ id: "user.full-name" })} | ||||||
|  | 															{...field} | ||||||
|  | 														/> | ||||||
|  | 														<label htmlFor="name"> | ||||||
|  | 															{intl.formatMessage({ id: "user.full-name" })} | ||||||
|  | 														</label> | ||||||
|  | 														{form.errors.name ? ( | ||||||
|  | 															<div className="invalid-feedback"> | ||||||
|  | 																{form.errors.name && form.touched.name | ||||||
|  | 																	? form.errors.name | ||||||
|  | 																	: null} | ||||||
|  | 															</div> | ||||||
|  | 														) : null} | ||||||
|  | 													</div> | ||||||
|  | 												)} | ||||||
|  | 											</Field> | ||||||
|  | 										</div> | ||||||
|  | 									</div> | ||||||
|  | 									<div className="col-lg-6"> | ||||||
|  | 										<div className="mb-3"> | ||||||
|  | 											<Field name="nickname" validate={validateString(1, 30)}> | ||||||
|  | 												{({ field, form }: any) => ( | ||||||
|  | 													<div className="form-floating mb-3"> | ||||||
|  | 														<input | ||||||
|  | 															id="nickname" | ||||||
|  | 															className={`form-control ${form.errors.nickname && form.touched.nickname ? "is-invalid" : ""}`} | ||||||
|  | 															placeholder={intl.formatMessage({ id: "user.nickname" })} | ||||||
|  | 															{...field} | ||||||
|  | 														/> | ||||||
|  | 														<label htmlFor="nickname"> | ||||||
|  | 															{intl.formatMessage({ id: "user.nickname" })} | ||||||
|  | 														</label> | ||||||
|  | 														{form.errors.nickname ? ( | ||||||
|  | 															<div className="invalid-feedback"> | ||||||
|  | 																{form.errors.nickname && form.touched.nickname | ||||||
|  | 																	? form.errors.nickname | ||||||
|  | 																	: null} | ||||||
|  | 															</div> | ||||||
|  | 														) : null} | ||||||
|  | 													</div> | ||||||
|  | 												)} | ||||||
|  | 											</Field> | ||||||
|  | 										</div> | ||||||
|  | 									</div> | ||||||
|  | 								</div> | ||||||
|  | 								<div className="mb-3"> | ||||||
|  | 									<Field name="email" validate={validateEmail()}> | ||||||
|  | 										{({ field, form }: any) => ( | ||||||
|  | 											<div className="form-floating mb-3"> | ||||||
|  | 												<input | ||||||
|  | 													id="email" | ||||||
|  | 													type="email" | ||||||
|  | 													className={`form-control ${form.errors.email && form.touched.email ? "is-invalid" : ""}`} | ||||||
|  | 													placeholder={intl.formatMessage({ id: "email-address" })} | ||||||
|  | 													{...field} | ||||||
|  | 												/> | ||||||
|  | 												<label htmlFor="email"> | ||||||
|  | 													{intl.formatMessage({ id: "email-address" })} | ||||||
|  | 												</label> | ||||||
|  | 												{form.errors.email ? ( | ||||||
|  | 													<div className="invalid-feedback"> | ||||||
|  | 														{form.errors.email && form.touched.email | ||||||
|  | 															? form.errors.email | ||||||
|  | 															: null} | ||||||
|  | 													</div> | ||||||
|  | 												) : null} | ||||||
|  | 											</div> | ||||||
|  | 										)} | ||||||
|  | 									</Field> | ||||||
|  | 								</div> | ||||||
|  | 								{currentUser && data && currentUser?.id !== data?.id ? ( | ||||||
|  | 									<div className="my-3"> | ||||||
|  | 										<h3 className="py-2">{intl.formatMessage({ id: "user.flags.title" })}</h3> | ||||||
|  | 										<div className="divide-y"> | ||||||
|  | 											<div> | ||||||
|  | 												<label className="row" htmlFor="isAdmin"> | ||||||
|  | 													<span className="col"> | ||||||
|  | 														{intl.formatMessage({ id: "role.admin" })} | ||||||
|  | 													</span> | ||||||
|  | 													<span className="col-auto"> | ||||||
|  | 														<Field name="isAdmin" type="checkbox"> | ||||||
|  | 															{({ field }: any) => ( | ||||||
|  | 																<label className="form-check form-check-single form-switch"> | ||||||
|  | 																	<input | ||||||
|  | 																		{...field} | ||||||
|  | 																		id="isAdmin" | ||||||
|  | 																		className="form-check-input" | ||||||
|  | 																		type="checkbox" | ||||||
|  | 																	/> | ||||||
|  | 																</label> | ||||||
|  | 															)} | ||||||
|  | 														</Field> | ||||||
|  | 													</span> | ||||||
|  | 												</label> | ||||||
|  | 											</div> | ||||||
|  | 											<div> | ||||||
|  | 												<label className="row" htmlFor="isDisabled"> | ||||||
|  | 													<span className="col"> | ||||||
|  | 														{intl.formatMessage({ id: "disabled" })} | ||||||
|  | 													</span> | ||||||
|  | 													<span className="col-auto"> | ||||||
|  | 														<Field name="isDisabled" type="checkbox"> | ||||||
|  | 															{({ field }: any) => ( | ||||||
|  | 																<label className="form-check form-check-single form-switch"> | ||||||
|  | 																	<input | ||||||
|  | 																		{...field} | ||||||
|  | 																		id="isDisabled" | ||||||
|  | 																		className="form-check-input" | ||||||
|  | 																		type="checkbox" | ||||||
|  | 																	/> | ||||||
|  | 																</label> | ||||||
|  | 															)} | ||||||
|  | 														</Field> | ||||||
|  | 													</span> | ||||||
|  | 												</label> | ||||||
|  | 											</div> | ||||||
|  | 										</div> | ||||||
|  | 									</div> | ||||||
|  | 								) : null} */} | ||||||
|  | 							</Modal.Body> | ||||||
|  | 							<Modal.Footer> | ||||||
|  | 								<Button data-bs-dismiss="modal" onClick={onClose} disabled={isSubmitting}> | ||||||
|  | 									{intl.formatMessage({ id: "cancel" })} | ||||||
|  | 								</Button> | ||||||
|  | 								<Button | ||||||
|  | 									type="submit" | ||||||
|  | 									actionType="primary" | ||||||
|  | 									className="ms-auto" | ||||||
|  | 									data-bs-dismiss="modal" | ||||||
|  | 									isLoading={isSubmitting} | ||||||
|  | 									disabled={isSubmitting} | ||||||
|  | 								> | ||||||
|  | 									{intl.formatMessage({ id: "save" })} | ||||||
|  | 								</Button> | ||||||
|  | 							</Modal.Footer> | ||||||
|  | 						</Form> | ||||||
|  | 					)} | ||||||
|  | 				</Formik> | ||||||
|  | 			)} | ||||||
|  | 		</Modal> | ||||||
|  | 	); | ||||||
|  | } | ||||||
| @@ -1,4 +1,5 @@ | |||||||
| export * from "./ChangePasswordModal"; | export * from "./ChangePasswordModal"; | ||||||
|  | export * from "./DeadHostModal"; | ||||||
| export * from "./DeleteConfirmModal"; | export * from "./DeleteConfirmModal"; | ||||||
| export * from "./EventDetailsModal"; | export * from "./EventDetailsModal"; | ||||||
| export * from "./PermissionsModal"; | export * from "./PermissionsModal"; | ||||||
|   | |||||||
| @@ -1,132 +0,0 @@ | |||||||
| import { IconDotsVertical, IconEdit, IconPower, IconSearch, IconTrash } from "@tabler/icons-react"; |  | ||||||
| import { Button } from "src/components"; |  | ||||||
| import { intl } from "src/locale"; |  | ||||||
|  |  | ||||||
| export default function CertificateTable() { |  | ||||||
| 	return ( |  | ||||||
| 		<div className="card mt-4"> |  | ||||||
| 			<div className="card-status-top bg-pink" /> |  | ||||||
| 			<div className="card-table"> |  | ||||||
| 				<div className="card-header"> |  | ||||||
| 					<div className="row w-full"> |  | ||||||
| 						<div className="col"> |  | ||||||
| 							<h2 className="mt-1 mb-0">{intl.formatMessage({ id: "certificates.title" })}</h2> |  | ||||||
| 						</div> |  | ||||||
| 						<div className="col-md-auto col-sm-12"> |  | ||||||
| 							<div className="ms-auto d-flex flex-wrap btn-list"> |  | ||||||
| 								<div className="input-group input-group-flat w-auto"> |  | ||||||
| 									<span className="input-group-text input-group-text-sm"> |  | ||||||
| 										<IconSearch size={16} /> |  | ||||||
| 									</span> |  | ||||||
| 									<input |  | ||||||
| 										id="advanced-table-search" |  | ||||||
| 										type="text" |  | ||||||
| 										className="form-control form-control-sm" |  | ||||||
| 										autoComplete="off" |  | ||||||
| 									/> |  | ||||||
| 								</div> |  | ||||||
| 								<Button size="sm" className="btn-pink"> |  | ||||||
| 									Add Certificate (dropdown) |  | ||||||
| 								</Button> |  | ||||||
| 							</div> |  | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 				<div id="advanced-table"> |  | ||||||
| 					<div className="table-responsive"> |  | ||||||
| 						<table className="table table-vcenter table-selectable"> |  | ||||||
| 							<thead> |  | ||||||
| 								<tr> |  | ||||||
| 									<th className="w-1" /> |  | ||||||
| 									<th> |  | ||||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> |  | ||||||
| 											Source |  | ||||||
| 										</button> |  | ||||||
| 									</th> |  | ||||||
| 									<th> |  | ||||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> |  | ||||||
| 											Destination |  | ||||||
| 										</button> |  | ||||||
| 									</th> |  | ||||||
| 									<th> |  | ||||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> |  | ||||||
| 											SSL |  | ||||||
| 										</button> |  | ||||||
| 									</th> |  | ||||||
| 									<th> |  | ||||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> |  | ||||||
| 											Access |  | ||||||
| 										</button> |  | ||||||
| 									</th> |  | ||||||
| 									<th> |  | ||||||
| 										<button type="button" className="table-sort d-flex justify-content-between"> |  | ||||||
| 											Status |  | ||||||
| 										</button> |  | ||||||
| 									</th> |  | ||||||
| 									<th className="w-1" /> |  | ||||||
| 								</tr> |  | ||||||
| 							</thead> |  | ||||||
| 							<tbody className="table-tbody"> |  | ||||||
| 								<tr> |  | ||||||
| 									<td data-label="Owner"> |  | ||||||
| 										<div className="d-flex py-1 align-items-center"> |  | ||||||
| 											<span |  | ||||||
| 												className="avatar avatar-2 me-2" |  | ||||||
| 												style={{ |  | ||||||
| 													backgroundImage: |  | ||||||
| 														"url(//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm)", |  | ||||||
| 												}} |  | ||||||
| 											/> |  | ||||||
| 										</div> |  | ||||||
| 									</td> |  | ||||||
| 									<td data-label="Destination"> |  | ||||||
| 										<div className="flex-fill"> |  | ||||||
| 											<div className="font-weight-medium"> |  | ||||||
| 												<span className="badge badge-lg domain-name">blog.jc21.com</span> |  | ||||||
| 											</div> |  | ||||||
| 											<div className="text-secondary mt-1">Created: 20th September 2024</div> |  | ||||||
| 										</div> |  | ||||||
| 									</td> |  | ||||||
| 									<td data-label="Source">http://172.17.0.1:3001</td> |  | ||||||
| 									<td data-label="SSL">Let's Encrypt</td> |  | ||||||
| 									<td data-label="Access">Public</td> |  | ||||||
| 									<td data-label="Status"> |  | ||||||
| 										<span className="badge bg-lime-lt">Online</span> |  | ||||||
| 									</td> |  | ||||||
| 									<td data-label="Status" className="text-end"> |  | ||||||
| 										<span className="dropdown"> |  | ||||||
| 											<button |  | ||||||
| 												type="button" |  | ||||||
| 												className="btn dropdown-toggle btn-action btn-sm px-1" |  | ||||||
| 												data-bs-boundary="viewport" |  | ||||||
| 												data-bs-toggle="dropdown" |  | ||||||
| 											> |  | ||||||
| 												<IconDotsVertical /> |  | ||||||
| 											</button> |  | ||||||
| 											<div className="dropdown-menu dropdown-menu-end"> |  | ||||||
| 												<span className="dropdown-header">Proxy Host #2</span> |  | ||||||
| 												<a className="dropdown-item" href="#"> |  | ||||||
| 													<IconEdit size={16} /> |  | ||||||
| 													Edit |  | ||||||
| 												</a> |  | ||||||
| 												<a className="dropdown-item" href="#"> |  | ||||||
| 													<IconPower size={16} /> |  | ||||||
| 													Disable |  | ||||||
| 												</a> |  | ||||||
| 												<div className="dropdown-divider" /> |  | ||||||
| 												<a className="dropdown-item" href="#"> |  | ||||||
| 													<IconTrash size={16} /> |  | ||||||
| 													Delete |  | ||||||
| 												</a> |  | ||||||
| 											</div> |  | ||||||
| 										</span> |  | ||||||
| 									</td> |  | ||||||
| 								</tr> |  | ||||||
| 							</tbody> |  | ||||||
| 						</table> |  | ||||||
| 					</div> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 		</div> |  | ||||||
| 	); |  | ||||||
| } |  | ||||||
| @@ -4,15 +4,18 @@ import { intl } from "src/locale"; | |||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
| 	tableInstance: ReactTable<any>; | 	tableInstance: ReactTable<any>; | ||||||
|  | 	onNew?: () => void; | ||||||
| } | } | ||||||
| export default function Empty({ tableInstance }: Props) { | export default function Empty({ tableInstance, onNew }: Props) { | ||||||
| 	return ( | 	return ( | ||||||
| 		<tr> | 		<tr> | ||||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||||
| 				<div className="text-center my-4"> | 				<div className="text-center my-4"> | ||||||
| 					<h2>{intl.formatMessage({ id: "dead-hosts.empty" })}</h2> | 					<h2>{intl.formatMessage({ id: "dead-hosts.empty" })}</h2> | ||||||
| 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | 					<p className="text-muted">{intl.formatMessage({ id: "empty-subtitle" })}</p> | ||||||
| 					<Button className="btn-red my-3">{intl.formatMessage({ id: "dead-hosts.add" })}</Button> | 					<Button className="btn-red my-3" onClick={onNew}> | ||||||
|  | 						{intl.formatMessage({ id: "dead-hosts.add" })} | ||||||
|  | 					</Button> | ||||||
| 				</div> | 				</div> | ||||||
| 			</td> | 			</td> | ||||||
| 		</tr> | 		</tr> | ||||||
|   | |||||||
| @@ -10,8 +10,10 @@ import Empty from "./Empty"; | |||||||
| interface Props { | interface Props { | ||||||
| 	data: DeadHost[]; | 	data: DeadHost[]; | ||||||
| 	isFetching?: boolean; | 	isFetching?: boolean; | ||||||
|  | 	onDelete?: (id: number) => void; | ||||||
|  | 	onNew?: () => void; | ||||||
| } | } | ||||||
| export default function Table({ data, isFetching }: Props) { | export default function Table({ data, isFetching, onDelete, onNew }: Props) { | ||||||
| 	const columnHelper = createColumnHelper<DeadHost>(); | 	const columnHelper = createColumnHelper<DeadHost>(); | ||||||
| 	const columns = useMemo( | 	const columns = useMemo( | ||||||
| 		() => [ | 		() => [ | ||||||
| @@ -78,7 +80,14 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 									{intl.formatMessage({ id: "action.disable" })} | 									{intl.formatMessage({ id: "action.disable" })} | ||||||
| 								</a> | 								</a> | ||||||
| 								<div className="dropdown-divider" /> | 								<div className="dropdown-divider" /> | ||||||
| 								<a className="dropdown-item" href="#"> | 								<a | ||||||
|  | 									className="dropdown-item" | ||||||
|  | 									href="#" | ||||||
|  | 									onClick={(e) => { | ||||||
|  | 										e.preventDefault(); | ||||||
|  | 										onDelete?.(info.row.original.id); | ||||||
|  | 									}} | ||||||
|  | 								> | ||||||
| 									<IconTrash size={16} /> | 									<IconTrash size={16} /> | ||||||
| 									{intl.formatMessage({ id: "action.delete" })} | 									{intl.formatMessage({ id: "action.delete" })} | ||||||
| 								</a> | 								</a> | ||||||
| @@ -91,7 +100,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
| 		], | 		], | ||||||
| 		[columnHelper], | 		[columnHelper, onDelete], | ||||||
| 	); | 	); | ||||||
|  |  | ||||||
| 	const tableInstance = useReactTable<DeadHost>({ | 	const tableInstance = useReactTable<DeadHost>({ | ||||||
| @@ -105,5 +114,7 @@ export default function Table({ data, isFetching }: Props) { | |||||||
| 		enableSortingRemoval: false, | 		enableSortingRemoval: false, | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	return <TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} />} />; | 	return ( | ||||||
|  | 		<TableLayout tableInstance={tableInstance} emptyState={<Empty tableInstance={tableInstance} onNew={onNew} />} /> | ||||||
|  | 	); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
| import { IconSearch } from "@tabler/icons-react"; | import { IconSearch } from "@tabler/icons-react"; | ||||||
|  | import { useState } from "react"; | ||||||
| import Alert from "react-bootstrap/Alert"; | import Alert from "react-bootstrap/Alert"; | ||||||
| import { Button, LoadingPage } from "src/components"; | import { Button, LoadingPage } from "src/components"; | ||||||
| import { useDeadHosts } from "src/hooks"; | import { useDeadHosts } from "src/hooks"; | ||||||
| import { intl } from "src/locale"; | import { intl } from "src/locale"; | ||||||
|  | import { DeadHostModal, DeleteConfirmModal } from "src/modals"; | ||||||
|  | import { showSuccess } from "src/notifications"; | ||||||
| import Table from "./Table"; | import Table from "./Table"; | ||||||
|  |  | ||||||
| export default function TableWrapper() { | export default function TableWrapper() { | ||||||
|  | 	const [deleteId, setDeleteId] = useState(0); | ||||||
|  | 	const [editId, setEditId] = useState(0 as number | "new"); | ||||||
| 	const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); | 	const { isFetching, isLoading, isError, error, data } = useDeadHosts(["owner", "certificate"]); | ||||||
|  |  | ||||||
| 	if (isLoading) { | 	if (isLoading) { | ||||||
| @@ -16,6 +21,11 @@ export default function TableWrapper() { | |||||||
| 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | 		return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const handleDelete = async () => { | ||||||
|  | 		// await deleteUser(deleteId); | ||||||
|  | 		showSuccess(intl.formatMessage({ id: "notification.host-deleted" })); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
| 	return ( | 	return ( | ||||||
| 		<div className="card mt-4"> | 		<div className="card mt-4"> | ||||||
| 			<div className="card-status-top bg-red" /> | 			<div className="card-status-top bg-red" /> | ||||||
| @@ -38,14 +48,30 @@ export default function TableWrapper() { | |||||||
| 										autoComplete="off" | 										autoComplete="off" | ||||||
| 									/> | 									/> | ||||||
| 								</div> | 								</div> | ||||||
| 								<Button size="sm" className="btn-red"> | 								<Button size="sm" className="btn-red" onClick={() => setEditId("new")}> | ||||||
| 									{intl.formatMessage({ id: "dead-hosts.add" })} | 									{intl.formatMessage({ id: "dead-hosts.add" })} | ||||||
| 								</Button> | 								</Button> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<Table data={data ?? []} isFetching={isFetching} /> | 				<Table | ||||||
|  | 					data={data ?? []} | ||||||
|  | 					isFetching={isFetching} | ||||||
|  | 					onDelete={(id: number) => setDeleteId(id)} | ||||||
|  | 					onNew={() => setEditId("new")} | ||||||
|  | 				/> | ||||||
|  | 				{editId ? <DeadHostModal id={editId} onClose={() => setEditId(0)} /> : null} | ||||||
|  | 				{deleteId ? ( | ||||||
|  | 					<DeleteConfirmModal | ||||||
|  | 						title={intl.formatMessage({ id: "user.delete.title" })} | ||||||
|  | 						onConfirm={handleDelete} | ||||||
|  | 						onClose={() => setDeleteId(0)} | ||||||
|  | 						invalidations={[["dead-hosts"], ["dead-host", deleteId]]} | ||||||
|  | 					> | ||||||
|  | 						{intl.formatMessage({ id: "user.delete.content" })} | ||||||
|  | 					</DeleteConfirmModal> | ||||||
|  | 				) : null} | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 	); | 	); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user