mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-10 14:24:17 +00:00
Merge branch 'dev' into feat/roles-and-user-multi-selectors
This commit is contained in:
@@ -6,12 +6,13 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func installCrowdsec(config Config) error {
|
func installCrowdsec(config Config, installDir string) error {
|
||||||
|
|
||||||
if err := stopContainers(config.InstallationContainerType); err != nil {
|
if err := stopContainers(config.InstallationContainerType); err != nil {
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
@@ -40,6 +41,8 @@ func installCrowdsec(config Config) error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupTraefikLogRotate(installDir)
|
||||||
|
|
||||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||||
fmt.Printf("Error copying docker service: %v\n", err)
|
fmt.Printf("Error copying docker service: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -208,3 +211,69 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
|||||||
fmt.Println("Added dependency of crowdsec to traefik")
|
fmt.Println("Added dependency of crowdsec to traefik")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupTraefikLogRotate writes a logrotate config for the Traefik access log
|
||||||
|
// that CrowdSec depends on. This is only needed when CrowdSec is installed
|
||||||
|
// because the default Pangolin install does not enable Traefik access logs.
|
||||||
|
//
|
||||||
|
// copytruncate is used so Traefik does not need to be restarted or sent a
|
||||||
|
// signal after rotation — it keeps writing to the same file descriptor while
|
||||||
|
// the rotated copy is made and the original is truncated in place.
|
||||||
|
func setupTraefikLogRotate(installDir string) {
|
||||||
|
const logrotateDir = "/etc/logrotate.d"
|
||||||
|
const logrotateFile = "/etc/logrotate.d/pangolin-traefik"
|
||||||
|
|
||||||
|
logPath := filepath.Join(installDir, "config/traefik/logs/access.log")
|
||||||
|
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Println("\n[logrotate] Skipping automatic logrotate setup: not running as root.")
|
||||||
|
fmt.Println("[logrotate] To prevent unbounded growth of the Traefik access log used by CrowdSec,")
|
||||||
|
fmt.Println("[logrotate] create the file /etc/logrotate.d/pangolin-traefik manually with:")
|
||||||
|
printLogrotateConfig(logPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := fmt.Sprintf(`# Logrotate config for Traefik access logs used by CrowdSec.
|
||||||
|
# Generated by the Pangolin installer. Safe to edit.
|
||||||
|
%s {
|
||||||
|
daily
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
|
`, logPath)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(logrotateDir, 0755); err != nil {
|
||||||
|
fmt.Printf("[logrotate] Warning: could not create %s: %v\n", logrotateDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(logrotateFile, []byte(config), 0644); err != nil {
|
||||||
|
fmt.Printf("[logrotate] Warning: could not write %s: %v\n", logrotateFile, err)
|
||||||
|
fmt.Println("[logrotate] Set it up manually:")
|
||||||
|
printLogrotateConfig(logPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[logrotate] Wrote logrotate config to %s\n", logrotateFile)
|
||||||
|
fmt.Println("[logrotate] Traefik access logs will be rotated daily, keeping 7 compressed copies.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printLogrotateConfig prints a logrotate config block to stdout so users can
|
||||||
|
// set it up manually when the installer cannot write to /etc.
|
||||||
|
func printLogrotateConfig(logPath string) {
|
||||||
|
fmt.Printf(`
|
||||||
|
%s {
|
||||||
|
daily
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
delaycompress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
copytruncate
|
||||||
|
}
|
||||||
|
`, logPath)
|
||||||
|
}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
err := installCrowdsec(config)
|
err := installCrowdsec(config, installDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error installing CrowdSec: %v\n", err)
|
fmt.Printf("Error installing CrowdSec: %v\n", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
heading: string;
|
heading: string;
|
||||||
previewText: string;
|
previewText: string;
|
||||||
summary: string;
|
summary: string;
|
||||||
statusLabel: string;
|
statusLabel: string | null;
|
||||||
statusColor: string;
|
statusColor: string | null;
|
||||||
} {
|
} {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case "site_online":
|
case "site_online":
|
||||||
@@ -63,8 +63,8 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
heading: "Site Status Changed",
|
heading: "Site Status Changed",
|
||||||
previewText: "A site in your organization has changed status.",
|
previewText: "A site in your organization has changed status.",
|
||||||
summary: "A site in your organization has changed status.",
|
summary: "A site in your organization has changed status.",
|
||||||
statusLabel: "Status Changed",
|
statusLabel: null,
|
||||||
statusColor: "#f59e0b"
|
statusColor: null
|
||||||
};
|
};
|
||||||
case "health_check_healthy":
|
case "health_check_healthy":
|
||||||
return {
|
return {
|
||||||
@@ -93,8 +93,8 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
"A health check in your organization has changed status.",
|
"A health check in your organization has changed status.",
|
||||||
summary:
|
summary:
|
||||||
"A health check in your organization has changed status.",
|
"A health check in your organization has changed status.",
|
||||||
statusLabel: "Status Changed",
|
statusLabel: null,
|
||||||
statusColor: "#f59e0b"
|
statusColor: null
|
||||||
};
|
};
|
||||||
case "resource_healthy":
|
case "resource_healthy":
|
||||||
return {
|
return {
|
||||||
@@ -120,8 +120,8 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
previewText:
|
previewText:
|
||||||
"A resource in your organization has changed status.",
|
"A resource in your organization has changed status.",
|
||||||
summary: "A resource in your organization has changed status.",
|
summary: "A resource in your organization has changed status.",
|
||||||
statusLabel: "Status Changed",
|
statusLabel: null,
|
||||||
statusColor: "#f59e0b"
|
statusColor: null
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
@@ -135,11 +135,26 @@ function getEventMeta(eventType: AlertEventType): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveToggleStatus(status: unknown): { label: string; color: string } {
|
||||||
|
switch (String(status).toLowerCase()) {
|
||||||
|
case "online":
|
||||||
|
return { label: "Online", color: "#16a34a" };
|
||||||
|
case "offline":
|
||||||
|
return { label: "Offline", color: "#dc2626" };
|
||||||
|
case "healthy":
|
||||||
|
return { label: "Healthy", color: "#16a34a" };
|
||||||
|
case "unhealthy":
|
||||||
|
return { label: "Unhealthy", color: "#dc2626" };
|
||||||
|
default:
|
||||||
|
return { label: String(status ?? "Unknown"), color: "#f59e0b" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatDataItems(
|
function formatDataItems(
|
||||||
data: Record<string, unknown>
|
data: Record<string, unknown>
|
||||||
): { label: string; value: React.ReactNode }[] {
|
): { label: string; value: React.ReactNode }[] {
|
||||||
return Object.entries(data)
|
return Object.entries(data)
|
||||||
.filter(([key]) => key !== "orgId")
|
.filter(([key]) => key !== "orgId" && key !== "status")
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
label: key
|
label: key
|
||||||
.replace(/([A-Z])/g, " $1")
|
.replace(/([A-Z])/g, " $1")
|
||||||
@@ -154,16 +169,36 @@ export const AlertNotification = (props: AlertNotificationProps) => {
|
|||||||
const meta = getEventMeta(eventType);
|
const meta = getEventMeta(eventType);
|
||||||
const dataItems = formatDataItems(data);
|
const dataItems = formatDataItems(data);
|
||||||
|
|
||||||
|
const isToggle =
|
||||||
|
eventType === "site_toggle" ||
|
||||||
|
eventType === "health_check_toggle" ||
|
||||||
|
eventType === "resource_toggle";
|
||||||
|
|
||||||
|
const resolvedStatus = isToggle
|
||||||
|
? resolveToggleStatus(data.status)
|
||||||
|
: meta.statusLabel != null
|
||||||
|
? { label: meta.statusLabel, color: meta.statusColor! }
|
||||||
|
: null;
|
||||||
|
|
||||||
const allItems: { label: string; value: React.ReactNode }[] = [
|
const allItems: { label: string; value: React.ReactNode }[] = [
|
||||||
{ label: "Organization", value: orgId },
|
{ label: "Organization", value: orgId },
|
||||||
{
|
...(resolvedStatus != null
|
||||||
label: "Status",
|
? [
|
||||||
value: (
|
{
|
||||||
<span style={{ color: meta.statusColor, fontWeight: 600 }}>
|
label: "Status",
|
||||||
{meta.statusLabel}
|
value: (
|
||||||
</span>
|
<span
|
||||||
)
|
style={{
|
||||||
},
|
color: resolvedStatus.color,
|
||||||
|
fontWeight: 600
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resolvedStatus.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{ label: "Time", value: new Date().toUTCString() },
|
{ label: "Time", value: new Date().toUTCString() },
|
||||||
...dataItems
|
...dataItems
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export const ClientResourceSchema = z
|
|||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
mode: z.enum(["host", "cidr", "http"]),
|
mode: z.enum(["host", "cidr", "http"]),
|
||||||
site: z.string(), // DEPRECATED IN FAVOR OF sites
|
site: z.string().optional(), // DEPRECATED IN FAVOR OF sites
|
||||||
sites: z.array(z.string()).optional().default([]),
|
sites: z.array(z.string()).optional().default([]),
|
||||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
// proxyPort: z.int().positive().optional(),
|
// proxyPort: z.int().positive().optional(),
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export async function fireHealthCheckHealthyAlert(
|
|||||||
healthCheckId,
|
healthCheckId,
|
||||||
data: {
|
data: {
|
||||||
healthCheckId,
|
healthCheckId,
|
||||||
|
status: "healthy",
|
||||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -133,6 +134,7 @@ export async function fireHealthCheckUnhealthyAlert(
|
|||||||
healthCheckId,
|
healthCheckId,
|
||||||
data: {
|
data: {
|
||||||
healthCheckId,
|
healthCheckId,
|
||||||
|
status: "unhealthy",
|
||||||
...(healthCheckName != null ? { healthCheckName } : {}),
|
...(healthCheckName != null ? { healthCheckName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export async function fireResourceHealthyAlert(
|
|||||||
resourceId,
|
resourceId,
|
||||||
data: {
|
data: {
|
||||||
resourceId,
|
resourceId,
|
||||||
|
status: "healthy",
|
||||||
...(resourceName != null ? { resourceName } : {}),
|
...(resourceName != null ? { resourceName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -115,6 +116,7 @@ export async function fireResourceUnhealthyAlert(
|
|||||||
resourceId,
|
resourceId,
|
||||||
data: {
|
data: {
|
||||||
resourceId,
|
resourceId,
|
||||||
|
status: "unhealthy",
|
||||||
...(resourceName != null ? { resourceName } : {}),
|
...(resourceName != null ? { resourceName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export async function fireSiteOnlineAlert(
|
|||||||
siteId,
|
siteId,
|
||||||
data: {
|
data: {
|
||||||
siteId,
|
siteId,
|
||||||
|
status: "online",
|
||||||
...(siteName != null ? { siteName } : {}),
|
...(siteName != null ? { siteName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
@@ -143,6 +144,7 @@ export async function fireSiteOfflineAlert(
|
|||||||
siteId,
|
siteId,
|
||||||
data: {
|
data: {
|
||||||
siteId,
|
siteId,
|
||||||
|
status: "offline",
|
||||||
...(siteName != null ? { siteName } : {}),
|
...(siteName != null ? { siteName } : {}),
|
||||||
...extra
|
...extra
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export async function sendAlertWebhook(
|
|||||||
const payload = {
|
const payload = {
|
||||||
event: context.eventType,
|
event: context.eventType,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
status: deriveStatus(context.eventType, context.data),
|
||||||
data: {
|
data: {
|
||||||
orgId: context.orgId,
|
orgId: context.orgId,
|
||||||
...context.data
|
...context.data
|
||||||
@@ -117,6 +118,38 @@ export async function sendAlertWebhook(
|
|||||||
throw lastError ?? new Error(`Alert webhook: all ${MAX_RETRIES} attempts failed for "${url}"`);
|
throw lastError ?? new Error(`Alert webhook: all ${MAX_RETRIES} attempts failed for "${url}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status derivation
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function deriveStatus(
|
||||||
|
eventType: AlertContext["eventType"],
|
||||||
|
data: Record<string, unknown>
|
||||||
|
): string {
|
||||||
|
switch (eventType) {
|
||||||
|
case "site_online":
|
||||||
|
return "online";
|
||||||
|
case "site_offline":
|
||||||
|
return "offline";
|
||||||
|
case "site_toggle":
|
||||||
|
return String(data.status ?? "unknown");
|
||||||
|
case "health_check_healthy":
|
||||||
|
case "resource_healthy":
|
||||||
|
return "healthy";
|
||||||
|
case "health_check_unhealthy":
|
||||||
|
case "resource_unhealthy":
|
||||||
|
return "unhealthy";
|
||||||
|
case "health_check_toggle":
|
||||||
|
case "resource_toggle":
|
||||||
|
return String(data.status ?? "unknown");
|
||||||
|
default: {
|
||||||
|
const _exhaustive: never = eventType;
|
||||||
|
void _exhaustive;
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Header construction (mirrors HttpLogDestination.buildHeaders)
|
// Header construction (mirrors HttpLogDestination.buildHeaders)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { Certificate, certificates, db, domains } from "@server/db";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { Transaction } from "@server/db";
|
import { Transaction } from "@server/db";
|
||||||
import { eq, or, and, like } from "drizzle-orm";
|
import { eq, or, and, like } from "drizzle-orm";
|
||||||
import privateConfig from "#private/lib/config";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a certificate exists for the given domain.
|
* Checks if a certificate exists for the given domain.
|
||||||
@@ -27,10 +26,6 @@ export async function createCertificate(
|
|||||||
domain: string,
|
domain: string,
|
||||||
trx: Transaction | typeof db
|
trx: Transaction | typeof db
|
||||||
) {
|
) {
|
||||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [domainRecord] = await trx
|
const [domainRecord] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(domains)
|
.from(domains)
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ async function query(domainId: string, domain: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let existing: any[] = [];
|
let existing: any[] = [];
|
||||||
if (domainRecord.type == "ns") {
|
if (domainRecord.type == "ns" || domainRecord.type == "wildcard") { // the manual "wildcard" domains can have wildcard certs
|
||||||
const domainLevelDown = domain.split(".").slice(1).join(".");
|
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||||
|
const wildcardPrefixed = `*.${domainLevelDown}`;
|
||||||
|
|
||||||
existing = await db
|
existing = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -64,7 +65,8 @@ async function query(domainId: string, domain: string) {
|
|||||||
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
||||||
or(
|
or(
|
||||||
eq(certificates.domain, domain),
|
eq(certificates.domain, domain),
|
||||||
eq(certificates.domain, domainLevelDown)
|
eq(certificates.domain, domainLevelDown),
|
||||||
|
eq(certificates.domain, wildcardPrefixed)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import createHttpError from "http-errors";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
|
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const createSiteResourceParamsSchema = z.strictObject({
|
const createSiteResourceParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -494,6 +496,10 @@ export async function createSiteResource(
|
|||||||
`Created site resource ${newSiteResource.siteResourceId} for org ${orgId}`
|
`Created site resource ${newSiteResource.siteResourceId} for org ${orgId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (ssl && mode === "http" && domainId && fullDomain && build != "oss") {
|
||||||
|
await createCertificate(domainId, fullDomain, db);
|
||||||
|
}
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: newSiteResource,
|
data: newSiteResource,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import type { Selectedsite } from "./site-selector";
|
|||||||
import { MachinesSelector } from "./machines-selector";
|
import { MachinesSelector } from "./machines-selector";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
import CertificateStatus from "@app/components/CertificateStatus";
|
||||||
|
|
||||||
// --- Helpers (shared) ---
|
// --- Helpers (shared) ---
|
||||||
|
|
||||||
@@ -1114,6 +1115,54 @@ export function InternalResourceForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ssl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex-1">
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="internal-resource-ssl"
|
||||||
|
label={t(
|
||||||
|
enableSslLabelKey
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
enableSslDescriptionKey
|
||||||
|
)}
|
||||||
|
checked={!!field.value}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
httpSectionDisabled
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{variant === "edit" &&
|
||||||
|
resource?.domainId &&
|
||||||
|
httpConfigFullDomain &&
|
||||||
|
form.watch("ssl") && (
|
||||||
|
<div className="flex items-center gap-1 pt-1">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t("certificateStatus")}:
|
||||||
|
</span>
|
||||||
|
<CertificateStatus
|
||||||
|
orgId={resource.orgId}
|
||||||
|
domainId={resource.domainId}
|
||||||
|
fullDomain={
|
||||||
|
httpConfigFullDomain
|
||||||
|
}
|
||||||
|
autoFetch={true}
|
||||||
|
showLabel={false}
|
||||||
|
polling={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{/* 4 cols because of the certs */}
|
{/* 4 cols because of the certs */}
|
||||||
<InfoSections
|
<InfoSections
|
||||||
cols={resource.http && env.flags.usePangolinDns ? 5 : 4}
|
cols={resource.http ? 5 : 4}
|
||||||
>
|
>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
|
||||||
@@ -43,7 +43,10 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>URL</InfoSectionTitle>
|
<InfoSectionTitle>URL</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<CopyToClipboard text={fullUrl} isLink={true} />
|
<CopyToClipboard
|
||||||
|
text={fullUrl}
|
||||||
|
isLink={true}
|
||||||
|
/>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
@@ -133,8 +136,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||||||
{/* Certificate Status Column */}
|
{/* Certificate Status Column */}
|
||||||
{resource.http &&
|
{resource.http &&
|
||||||
resource.domainId &&
|
resource.domainId &&
|
||||||
resource.fullDomain &&
|
resource.fullDomain && (
|
||||||
env.flags.usePangolinDns && (
|
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("certificateStatus", {
|
{t("certificateStatus", {
|
||||||
|
|||||||
Reference in New Issue
Block a user