Merge branch 'dev' of github.com:fosrl/pangolin into dev
51
README.md
|
@ -25,6 +25,10 @@ _Your own self-hosted zero trust tunnel._
|
||||||
<a href="https://docs.fossorial.io">
|
<a href="https://docs.fossorial.io">
|
||||||
Full Documentation
|
Full Documentation
|
||||||
</a>
|
</a>
|
||||||
|
<span> | </span>
|
||||||
|
<a href="mailto:numbat@fossorial.io">
|
||||||
|
Contact Us
|
||||||
|
</a>
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -68,41 +72,17 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected
|
||||||
### Easy Deployment
|
### Easy Deployment
|
||||||
|
|
||||||
- Run on any cloud provider or on-premises.
|
- Run on any cloud provider or on-premises.
|
||||||
- Docker Compose based setup for simplified deployment.
|
- **Docker Compose based setup** for simplified deployment.
|
||||||
- Future-proof installation script for streamlined setup and feature additions.
|
- Future-proof installation script for streamlined setup and feature additions.
|
||||||
- Use your preferred WireGuard client to connect, or use Newt, our custom user space client for the best experience.
|
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
|
||||||
|
|
||||||
### Modular Design
|
### Modular Design
|
||||||
|
|
||||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [Fail2Ban](https://plugins.traefik.io/plugins/628c9ebcffc0cd18356a979f/fail2-ban) or [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin).
|
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](github.com/PascalMinder/geoblock).
|
||||||
|
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
||||||
- Attach as many sites to the central server as you wish.
|
- Attach as many sites to the central server as you wish.
|
||||||
|
|
||||||
## Screenshots
|
<img src="public/screenshots/collage.png" alt="Collage"/>
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><img src="public/screenshots/sites.png" alt="Sites Example" width="200"/></td>
|
|
||||||
<td align="center"><img src="public/screenshots/users.png" alt="Users Example" width="200"/></td>
|
|
||||||
<td align="center"><img src="public/screenshots/share-link.png" alt="Share Link Example" width="200"/></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><b>Sites</b></td>
|
|
||||||
<td align="center"><b>Users</b></td>
|
|
||||||
<td align="center"><b>Share Link</b></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><img src="public/screenshots/auth.png" alt="Authentication Example" width="200"/></td>
|
|
||||||
<td align="center"><img src="public/screenshots/connectivity.png" alt="Connectivity Example" width="200"/></td>
|
|
||||||
<td align="center"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"><b>Authentication</b></td>
|
|
||||||
<td align="center"><b>Connectivity</b></td>
|
|
||||||
<td align="center"><b></b></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Deployment and Usage Example
|
## Deployment and Usage Example
|
||||||
|
|
||||||
|
@ -112,7 +92,7 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal!
|
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can likely get a **VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**. That's a great deal!
|
||||||
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you sign up using [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
|
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
|
||||||
|
|
||||||
2. **Domain Configuration**:
|
2. **Domain Configuration**:
|
||||||
|
|
||||||
|
@ -123,10 +103,10 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected
|
||||||
- Install Newt or use another WireGuard client on private sites.
|
- Install Newt or use another WireGuard client on private sites.
|
||||||
- Automatically establish a connection from these sites to the central server.
|
- Automatically establish a connection from these sites to the central server.
|
||||||
|
|
||||||
4. **Configure Users & Roles**
|
4. **Expose Resources**:
|
||||||
|
|
||||||
- Define organizations and invite users.
|
- Add resources to the central server and configure access control rules.
|
||||||
- Implement user- or role-based permissions to control resource access.
|
- Access these resources securely from anywhere.
|
||||||
|
|
||||||
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
||||||
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
|
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
|
||||||
|
@ -134,6 +114,11 @@ _Sites page of Pangolin dashboard (dark mode) showing multiple tunnels connected
|
||||||
**Use Case Example - IoT Networks**:
|
**Use Case Example - IoT Networks**:
|
||||||
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
|
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="public/screenshots/resources.png" alt="Resources"/>
|
||||||
|
|
||||||
|
_Resources page of Pangolin dashboard (dark mode) showing HTTPS and TCP resources with access control rules._
|
||||||
|
|
||||||
## Similar Projects and Inspirations
|
## Similar Projects and Inspirations
|
||||||
|
|
||||||
**Cloudflare Tunnels**:
|
**Cloudflare Tunnels**:
|
||||||
|
|
Before Width: | Height: | Size: 577 KiB |
BIN
public/screenshots/collage.png
Normal file
After Width: | Height: | Size: 876 KiB |
Before Width: | Height: | Size: 447 KiB |
BIN
public/screenshots/resources.png
Normal file
After Width: | Height: | Size: 702 KiB |
Before Width: | Height: | Size: 484 KiB |
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 731 KiB |
Before Width: | Height: | Size: 415 KiB |
|
@ -129,18 +129,19 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
|
||||||
|
|
||||||
export function serializeSessionCookie(
|
export function serializeSessionCookie(
|
||||||
token: string,
|
token: string,
|
||||||
isSecure: boolean
|
isSecure: boolean,
|
||||||
|
expiresAt: Date
|
||||||
): string {
|
): string {
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/;`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,12 +167,19 @@ export function serializeResourceSessionCookie(
|
||||||
cookieName: string,
|
cookieName: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
token: string,
|
token: string,
|
||||||
isHttp: boolean = false
|
isHttp: boolean = false,
|
||||||
|
expiresAt?: Date
|
||||||
): string {
|
): string {
|
||||||
if (!isHttp) {
|
if (!isHttp) {
|
||||||
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
|
if (expiresAt === undefined) {
|
||||||
|
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`;
|
||||||
|
}
|
||||||
|
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`;
|
||||||
} else {
|
} else {
|
||||||
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
|
if (expiresAt === undefined) {
|
||||||
|
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`;
|
||||||
|
}
|
||||||
|
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from "@server/emails/sendEmail";
|
||||||
import nodemailer from "nodemailer";
|
import nodemailer from "nodemailer";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
|
@ -13,7 +14,7 @@ function createEmailClient() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodemailer.createTransport({
|
const settings = {
|
||||||
host: emailConfig.smtp_host,
|
host: emailConfig.smtp_host,
|
||||||
port: emailConfig.smtp_port,
|
port: emailConfig.smtp_port,
|
||||||
secure: emailConfig.smtp_secure || false,
|
secure: emailConfig.smtp_secure || false,
|
||||||
|
@ -21,7 +22,15 @@ function createEmailClient() {
|
||||||
user: emailConfig.smtp_user,
|
user: emailConfig.smtp_user,
|
||||||
pass: emailConfig.smtp_pass
|
pass: emailConfig.smtp_pass
|
||||||
}
|
}
|
||||||
});
|
} as SMTPTransport.Options;
|
||||||
|
|
||||||
|
if (emailConfig.smtp_tls_reject_unathorized !== undefined) {
|
||||||
|
settings.tls = {
|
||||||
|
rejectUnauthorized: emailConfig.smtp_tls_reject_unathorized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodemailer.createTransport(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const emailClient = createEmailClient();
|
export const emailClient = createEmailClient();
|
||||||
|
|
|
@ -160,6 +160,7 @@ const configSchema = z.object({
|
||||||
smtp_user: z.string().optional(),
|
smtp_user: z.string().optional(),
|
||||||
smtp_pass: z.string().optional(),
|
smtp_pass: z.string().optional(),
|
||||||
smtp_secure: z.boolean().optional(),
|
smtp_secure: z.boolean().optional(),
|
||||||
|
smtp_tls_reject_unathorized: z.boolean().optional(),
|
||||||
no_reply: z.string().email().optional()
|
no_reply: z.string().email().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
@ -184,7 +185,8 @@ const configSchema = z.object({
|
||||||
disable_signup_without_invite: z.boolean().optional(),
|
disable_signup_without_invite: z.boolean().optional(),
|
||||||
disable_user_create_org: z.boolean().optional(),
|
disable_user_create_org: z.boolean().optional(),
|
||||||
allow_raw_resources: z.boolean().optional(),
|
allow_raw_resources: z.boolean().optional(),
|
||||||
allow_base_domain_resources: z.boolean().optional()
|
allow_base_domain_resources: z.boolean().optional(),
|
||||||
|
allow_local_sites: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
|
@ -137,9 +137,13 @@ export async function login(
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createSession(token, existingUser.userId);
|
const sess = await createSession(token, existingUser.userId);
|
||||||
const isSecure = req.protocol === "https";
|
const isSecure = req.protocol === "https";
|
||||||
const cookie = serializeSessionCookie(token, isSecure);
|
const cookie = serializeSessionCookie(
|
||||||
|
token,
|
||||||
|
isSecure,
|
||||||
|
new Date(sess.expiresAt)
|
||||||
|
);
|
||||||
|
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
|
|
|
@ -170,9 +170,13 @@ export async function signup(
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createSession(token, userId);
|
const sess = await createSession(token, userId);
|
||||||
const isSecure = req.protocol === "https";
|
const isSecure = req.protocol === "https";
|
||||||
const cookie = serializeSessionCookie(token, isSecure);
|
const cookie = serializeSessionCookie(
|
||||||
|
token,
|
||||||
|
isSecure,
|
||||||
|
new Date(sess.expiresAt)
|
||||||
|
);
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
|
|
||||||
if (config.getRawConfig().flags?.require_email_verification) {
|
if (config.getRawConfig().flags?.require_email_verification) {
|
||||||
|
|
|
@ -102,6 +102,8 @@ export async function exchangeSession(
|
||||||
|
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
|
|
||||||
|
let expiresAt: number | null = null;
|
||||||
|
|
||||||
if (requestSession.userSessionId) {
|
if (requestSession.userSessionId) {
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
.select()
|
.select()
|
||||||
|
@ -118,6 +120,7 @@ export async function exchangeSession(
|
||||||
expiresAt: res.expiresAt,
|
expiresAt: res.expiresAt,
|
||||||
sessionLength: SESSION_COOKIE_EXPIRES
|
sessionLength: SESSION_COOKIE_EXPIRES
|
||||||
});
|
});
|
||||||
|
expiresAt = res.expiresAt;
|
||||||
}
|
}
|
||||||
} else if (requestSession.accessTokenId) {
|
} else if (requestSession.accessTokenId) {
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
|
@ -140,8 +143,12 @@ export async function exchangeSession(
|
||||||
expiresAt: res.expiresAt,
|
expiresAt: res.expiresAt,
|
||||||
sessionLength: res.sessionLength
|
sessionLength: res.sessionLength
|
||||||
});
|
});
|
||||||
|
expiresAt = res.expiresAt;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const expires = new Date(
|
||||||
|
Date.now() + SESSION_COOKIE_EXPIRES
|
||||||
|
).getTime();
|
||||||
await createResourceSession({
|
await createResourceSession({
|
||||||
token,
|
token,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
|
@ -152,11 +159,10 @@ export async function exchangeSession(
|
||||||
whitelistId: requestSession.whitelistId,
|
whitelistId: requestSession.whitelistId,
|
||||||
accessTokenId: requestSession.accessTokenId,
|
accessTokenId: requestSession.accessTokenId,
|
||||||
doNotExtend: false,
|
doNotExtend: false,
|
||||||
expiresAt: new Date(
|
expiresAt: expires,
|
||||||
Date.now() + SESSION_COOKIE_EXPIRES
|
|
||||||
).getTime(),
|
|
||||||
sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES
|
sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES
|
||||||
});
|
});
|
||||||
|
expiresAt = expires;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||||
|
@ -164,7 +170,8 @@ export async function exchangeSession(
|
||||||
cookieName,
|
cookieName,
|
||||||
resource.fullDomain!,
|
resource.fullDomain!,
|
||||||
token,
|
token,
|
||||||
!resource.ssl
|
!resource.ssl,
|
||||||
|
expiresAt ? new Date(expiresAt) : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug(JSON.stringify("Exchange cookie: " + cookie));
|
logger.debug(JSON.stringify("Exchange cookie: " + cookie));
|
||||||
|
|
|
@ -384,7 +384,7 @@ async function createAccessTokenSession(
|
||||||
tokenItem: ResourceAccessToken
|
tokenItem: ResourceAccessToken
|
||||||
) {
|
) {
|
||||||
const token = generateSessionToken();
|
const token = generateSessionToken();
|
||||||
await createResourceSession({
|
const sess = await createResourceSession({
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
token,
|
token,
|
||||||
accessTokenId: tokenItem.accessTokenId,
|
accessTokenId: tokenItem.accessTokenId,
|
||||||
|
@ -397,7 +397,8 @@ async function createAccessTokenSession(
|
||||||
cookieName,
|
cookieName,
|
||||||
resource.fullDomain!,
|
resource.fullDomain!,
|
||||||
token,
|
token,
|
||||||
!resource.ssl
|
!resource.ssl,
|
||||||
|
new Date(sess.expiresAt)
|
||||||
);
|
);
|
||||||
res.appendHeader("Set-Cookie", cookie);
|
res.appendHeader("Set-Cookie", cookie);
|
||||||
logger.debug("Access token is valid, creating new session");
|
logger.debug("Access token is valid, creating new session");
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
@ -24,11 +24,11 @@ import {
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
import { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
|
@ -40,13 +40,13 @@ type CreateRoleFormProps = {
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string({ message: "Name is required" }).max(32),
|
name: z.string({ message: "Name is required" }).max(32),
|
||||||
description: z.string().max(255).optional(),
|
description: z.string().max(255).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function CreateRoleForm({
|
export default function CreateRoleForm({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
afterCreate,
|
afterCreate
|
||||||
}: CreateRoleFormProps) {
|
}: CreateRoleFormProps) {
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
|
@ -58,8 +58,8 @@ export default function CreateRoleForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
@ -70,7 +70,7 @@ export default function CreateRoleForm({
|
||||||
`/org/${org?.org.orgId}/role`,
|
`/org/${org?.org.orgId}/role`,
|
||||||
{
|
{
|
||||||
name: values.name,
|
name: values.name,
|
||||||
description: values.description,
|
description: values.description
|
||||||
} as CreateRoleBody
|
} as CreateRoleBody
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -80,7 +80,7 @@ export default function CreateRoleForm({
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while creating the role."
|
"An error occurred while creating the role."
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ export default function CreateRoleForm({
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "Role created",
|
title: "Role created",
|
||||||
description: "The role has been successfully created.",
|
description: "The role has been successfully created."
|
||||||
});
|
});
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
|
@ -135,9 +135,7 @@ export default function CreateRoleForm({
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Role Name</FormLabel>
|
<FormLabel>Role Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -150,9 +148,7 @@ export default function CreateRoleForm({
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -162,6 +158,9 @@ export default function CreateRoleForm({
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="create-role-form"
|
form="create-role-form"
|
||||||
|
@ -170,9 +169,6 @@ export default function CreateRoleForm({
|
||||||
>
|
>
|
||||||
Create Role
|
Create Role
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
@ -23,7 +23,7 @@ import {
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
|
@ -32,10 +32,10 @@ import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { RoleRow } from "./RolesTable";
|
import { RoleRow } from "./RolesTable";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
|
@ -47,14 +47,14 @@ type CreateRoleFormProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
newRoleId: z.string({ message: "New role is required" }),
|
newRoleId: z.string({ message: "New role is required" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function DeleteRoleForm({
|
export default function DeleteRoleForm({
|
||||||
open,
|
open,
|
||||||
roleToDelete,
|
roleToDelete,
|
||||||
setOpen,
|
setOpen,
|
||||||
afterDelete,
|
afterDelete
|
||||||
}: CreateRoleFormProps) {
|
}: CreateRoleFormProps) {
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
|
@ -66,9 +66,9 @@ export default function DeleteRoleForm({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchRoles() {
|
async function fetchRoles() {
|
||||||
const res = await api
|
const res = await api
|
||||||
.get<AxiosResponse<ListRolesResponse>>(
|
.get<
|
||||||
`/org/${org?.org.orgId}/roles`
|
AxiosResponse<ListRolesResponse>
|
||||||
)
|
>(`/org/${org?.org.orgId}/roles`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast({
|
toast({
|
||||||
|
@ -77,7 +77,7 @@ export default function DeleteRoleForm({
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while fetching the roles"
|
"An error occurred while fetching the roles"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ export default function DeleteRoleForm({
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
newRoleId: "",
|
newRoleId: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
@ -106,8 +106,8 @@ export default function DeleteRoleForm({
|
||||||
const res = await api
|
const res = await api
|
||||||
.delete(`/role/${roleToDelete.roleId}`, {
|
.delete(`/role/${roleToDelete.roleId}`, {
|
||||||
data: {
|
data: {
|
||||||
roleId: values.newRoleId,
|
roleId: values.newRoleId
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
|
@ -116,7 +116,7 @@ export default function DeleteRoleForm({
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while removing the role."
|
"An error occurred while removing the role."
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ export default function DeleteRoleForm({
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "Role removed",
|
title: "Role removed",
|
||||||
description: "The role has been successfully removed.",
|
description: "The role has been successfully removed."
|
||||||
});
|
});
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
|
@ -214,6 +214,9 @@ export default function DeleteRoleForm({
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="remove-role-form"
|
form="remove-role-form"
|
||||||
|
@ -222,9 +225,6 @@ export default function DeleteRoleForm({
|
||||||
>
|
>
|
||||||
Remove Role
|
Remove Role
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
|
@ -194,9 +194,7 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -340,6 +338,9 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="invite-user-form"
|
form="invite-user-form"
|
||||||
|
@ -348,9 +349,6 @@ export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) {
|
||||||
>
|
>
|
||||||
Create Invitation
|
Create Invitation
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -185,7 +185,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||||
<Link
|
<Link
|
||||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outline"} className="ml-2">
|
<Button variant={"outlinePrimary"} className="ml-2">
|
||||||
Manage
|
Manage
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0.5 select-none mb-6">
|
<div className="space-y-0.5 mb-6">
|
||||||
<h2 className="text-2xl font-bold tracking-tight">
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
User {user?.email}
|
User {user?.email}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -73,7 +73,6 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
|
||||||
|
|
||||||
<SidebarSettings
|
<SidebarSettings
|
||||||
sidebarNavItems={sidebarNavItems}
|
sidebarNavItems={sidebarNavItems}
|
||||||
limitWidth={true}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</SidebarSettings>
|
</SidebarSettings>
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { TopbarNav } from "@app/components/TopbarNav";
|
import { TopbarNav } from "@app/components/TopbarNav";
|
||||||
import { Cog, Combine, Link, Settings, Users, Waypoints } from "lucide-react";
|
import {
|
||||||
|
Cog,
|
||||||
|
Combine,
|
||||||
|
LinkIcon,
|
||||||
|
Settings,
|
||||||
|
Users,
|
||||||
|
Waypoints
|
||||||
|
} from "lucide-react";
|
||||||
import { Header } from "@app/components/Header";
|
import { Header } from "@app/components/Header";
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
@ -11,6 +18,14 @@ import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import { GetOrgUserResponse } from "@server/routers/user";
|
import { GetOrgUserResponse } from "@server/routers/user";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator
|
||||||
|
} from "@app/components/ui/breadcrumb";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
@ -38,7 +53,7 @@ const topNavItems = [
|
||||||
{
|
{
|
||||||
title: "Shareable Links",
|
title: "Shareable Links",
|
||||||
href: "/{orgId}/settings/share-links",
|
href: "/{orgId}/settings/share-links",
|
||||||
icon: <Link className="h-4 w-4" />
|
icon: <LinkIcon className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "General",
|
title: "General",
|
||||||
|
@ -95,19 +110,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full border-b bg-card select-none sm:px-0 px-3 fixed top-0 z-10">
|
<div className="w-full bg-card sm:px-0 px-3 fixed top-0 z-10">
|
||||||
<div className="container mx-auto flex flex-col content-between">
|
<div className="border-b">
|
||||||
<div className="my-4">
|
<div className="container mx-auto flex flex-col content-between">
|
||||||
<UserProvider user={user}>
|
<div className="my-4">
|
||||||
<Header orgId={params.orgId} orgs={orgs} />
|
<UserProvider user={user}>
|
||||||
</UserProvider>
|
<Header orgId={params.orgId} orgs={orgs} />
|
||||||
|
</UserProvider>
|
||||||
|
</div>
|
||||||
|
<TopbarNav items={topNavItems} orgId={params.orgId} />
|
||||||
</div>
|
</div>
|
||||||
<TopbarNav items={topNavItems} orgId={params.orgId} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container mx-auto sm:px-0 px-3 pt-[165px]">
|
<div className="container mx-auto sm:px-0 px-3 pt-[155px]">
|
||||||
{children}
|
<div className="container mx-auto sm:px-0 px-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,6 +66,8 @@ import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
import { Label } from "@app/components/ui/label";
|
import { Label } from "@app/components/ui/label";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain";
|
import { ListDomainsResponse } from "@server/routers/domain";
|
||||||
|
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
|
||||||
|
import { StrategySelect } from "@app/components/StrategySelect";
|
||||||
|
|
||||||
const createResourceFormSchema = z
|
const createResourceFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -140,6 +142,7 @@ export default function CreateResourceForm({
|
||||||
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
||||||
"subdomain"
|
"subdomain"
|
||||||
);
|
);
|
||||||
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
|
|
||||||
const form = useForm<CreateResourceFormValues>({
|
const form = useForm<CreateResourceFormValues>({
|
||||||
resolver: zodResolver(createResourceFormSchema),
|
resolver: zodResolver(createResourceFormSchema),
|
||||||
|
@ -215,8 +218,17 @@ export default function CreateResourceForm({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchSites();
|
const load = async () => {
|
||||||
fetchDomains();
|
setLoadingPage(true);
|
||||||
|
|
||||||
|
await fetchSites();
|
||||||
|
await fetchDomains();
|
||||||
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
|
|
||||||
|
setLoadingPage(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
async function onSubmit(data: CreateResourceFormValues) {
|
async function onSubmit(data: CreateResourceFormValues) {
|
||||||
|
@ -231,7 +243,7 @@ export default function CreateResourceForm({
|
||||||
protocol: data.protocol,
|
protocol: data.protocol,
|
||||||
proxyPort: data.http ? undefined : data.proxyPort,
|
proxyPort: data.http ? undefined : data.proxyPort,
|
||||||
siteId: data.siteId,
|
siteId: data.siteId,
|
||||||
isBaseDomain: data.http ? undefined : data.isBaseDomain
|
isBaseDomain: data.http ? data.isBaseDomain : undefined
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -253,6 +265,7 @@ export default function CreateResourceForm({
|
||||||
goToResource(id);
|
goToResource(id);
|
||||||
} else {
|
} else {
|
||||||
setShowSnippets(true);
|
setShowSnippets(true);
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,6 +275,21 @@ export default function CreateResourceForm({
|
||||||
router.push(`/${orgId}/settings/resources/${id || resourceId}`);
|
router.push(`/${orgId}/settings/resources/${id || resourceId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const launchOptions = [
|
||||||
|
{
|
||||||
|
id: "http",
|
||||||
|
title: "HTTPS Resource",
|
||||||
|
description:
|
||||||
|
"Proxy requests to your app over HTTPS using a subdomain or base domain."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "raw",
|
||||||
|
title: "Raw TCP/UDP Resource",
|
||||||
|
description:
|
||||||
|
"Proxy requests to your app over TCP/UDP using a port number."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Credenza
|
<Credenza
|
||||||
|
@ -282,236 +310,458 @@ export default function CreateResourceForm({
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
{!showSnippets && (
|
{loadingPage ? (
|
||||||
<Form {...form} key={formKey}>
|
<LoaderPlaceholder height="300px" />
|
||||||
<form
|
) : (
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<div>
|
||||||
className="space-y-4"
|
{!showSnippets && (
|
||||||
id="create-resource-form"
|
<Form {...form} key={formKey}>
|
||||||
>
|
<form
|
||||||
{!env.flags.allowRawResources || (
|
onSubmit={form.handleSubmit(
|
||||||
<FormField
|
onSubmit
|
||||||
control={form.control}
|
|
||||||
name="http"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">
|
|
||||||
HTTP Resource
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Toggle if this is an
|
|
||||||
HTTP resource or a
|
|
||||||
raw TCP/UDP
|
|
||||||
resource.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
onCheckedChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
className="space-y-4"
|
||||||
)}
|
id="create-resource-form"
|
||||||
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
This is display name for the
|
|
||||||
resource.
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.watch("http") &&
|
|
||||||
env.flags.allowBaseDomainResources && (
|
|
||||||
<div>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex space-x-4"
|
|
||||||
defaultValue={domainType}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
setDomainType(
|
|
||||||
val as any
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"isBaseDomain",
|
|
||||||
val === "basedomain"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="subdomain"
|
|
||||||
id="r1"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="r1">
|
|
||||||
Subdomain
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="basedomain"
|
|
||||||
id="r2"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="r2">
|
|
||||||
Base Domain
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.watch("http") && (
|
|
||||||
<>
|
|
||||||
{domainType === "subdomain" ? (
|
|
||||||
<div className="w-fill space-y-2">
|
|
||||||
{!env.flags
|
|
||||||
.allowBaseDomainResources && (
|
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Subdomain
|
Name
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
)}
|
<FormControl>
|
||||||
<div className="flex">
|
<Input {...field} />
|
||||||
<div className="w-full mr-1">
|
</FormControl>
|
||||||
<FormField
|
<FormMessage />
|
||||||
control={
|
</FormItem>
|
||||||
form.control
|
)}
|
||||||
}
|
/>
|
||||||
name="subdomain"
|
|
||||||
render={({
|
<FormField
|
||||||
field
|
control={form.control}
|
||||||
}) => (
|
name="siteId"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input
|
<FormItem className="flex flex-col">
|
||||||
{...field}
|
<FormLabel>
|
||||||
className="text-right"
|
Site
|
||||||
placeholder="Enter subdomain"
|
</FormLabel>
|
||||||
/>
|
<Popover>
|
||||||
</FormControl>
|
<PopoverTrigger
|
||||||
)}
|
asChild
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-1/2">
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
form.control
|
|
||||||
}
|
|
||||||
name="domainId"
|
|
||||||
render={({
|
|
||||||
field
|
|
||||||
}) => (
|
|
||||||
<FormItem>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{baseDomains.map(
|
|
||||||
(
|
|
||||||
option
|
|
||||||
) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
>
|
|
||||||
.
|
|
||||||
{
|
|
||||||
option.baseDomain
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="domainId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<Button
|
||||||
<SelectValue />
|
variant="outline"
|
||||||
</SelectTrigger>
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"justify-between",
|
||||||
|
!field.value &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? sites.find(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) =>
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
)
|
||||||
|
?.name
|
||||||
|
: "Select site"}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
</PopoverTrigger>
|
||||||
{baseDomains.map(
|
<PopoverContent className="p-0">
|
||||||
(
|
<Command>
|
||||||
option
|
<CommandInput placeholder="Search site" />
|
||||||
) => (
|
<CommandList>
|
||||||
<SelectItem
|
<CommandEmpty>
|
||||||
key={
|
No
|
||||||
option.domainId
|
site
|
||||||
}
|
found.
|
||||||
value={
|
</CommandEmpty>
|
||||||
option.domainId
|
<CommandGroup>
|
||||||
}
|
{sites.map(
|
||||||
>
|
(
|
||||||
{
|
site
|
||||||
option.baseDomain
|
) => (
|
||||||
}
|
<CommandItem
|
||||||
</SelectItem>
|
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
||||||
)
|
key={
|
||||||
)}
|
site.siteId
|
||||||
</SelectContent>
|
}
|
||||||
</Select>
|
onSelect={() => {
|
||||||
<FormMessage />
|
form.setValue(
|
||||||
</FormItem>
|
"siteId",
|
||||||
)}
|
site.siteId
|
||||||
/>
|
);
|
||||||
)}
|
}}
|
||||||
</>
|
>
|
||||||
)}
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
site.name
|
||||||
|
}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
This site will
|
||||||
|
provide connectivity
|
||||||
|
to the resource.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!env.flags.allowRawResources || (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
Resource Type
|
||||||
|
</FormLabel>
|
||||||
|
<StrategySelect
|
||||||
|
options={launchOptions}
|
||||||
|
defaultValue="http"
|
||||||
|
onChange={(value) =>
|
||||||
|
form.setValue(
|
||||||
|
"http",
|
||||||
|
value === "http"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FormDescription>
|
||||||
|
You cannot change the
|
||||||
|
type of resource after
|
||||||
|
creation.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.watch("http") &&
|
||||||
|
env.flags
|
||||||
|
.allowBaseDomainResources && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isBaseDomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Domain Type
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
domainType
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
val
|
||||||
|
) => {
|
||||||
|
setDomainType(
|
||||||
|
val ===
|
||||||
|
"basedomain"
|
||||||
|
? "basedomain"
|
||||||
|
: "subdomain"
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"isBaseDomain",
|
||||||
|
val ===
|
||||||
|
"basedomain"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="subdomain">
|
||||||
|
Subdomain
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="basedomain">
|
||||||
|
Base
|
||||||
|
Domain
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.watch("http") && (
|
||||||
|
<>
|
||||||
|
{domainType ===
|
||||||
|
"subdomain" ? (
|
||||||
|
<div className="w-fill space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
Subdomain
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormField
|
||||||
|
control={
|
||||||
|
form.control
|
||||||
|
}
|
||||||
|
name="subdomain"
|
||||||
|
render={({
|
||||||
|
field
|
||||||
|
}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
className="border-r-0 rounded-r-none"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormField
|
||||||
|
control={
|
||||||
|
form.control
|
||||||
|
}
|
||||||
|
name="domainId"
|
||||||
|
render={({
|
||||||
|
field
|
||||||
|
}) => (
|
||||||
|
<FormItem>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger className="rounded-l-none">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{baseDomains.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
.
|
||||||
|
{
|
||||||
|
option.baseDomain
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FormField
|
||||||
|
control={
|
||||||
|
form.control
|
||||||
|
}
|
||||||
|
name="domainId"
|
||||||
|
render={({
|
||||||
|
field
|
||||||
|
}) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Base
|
||||||
|
Domain
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{baseDomains.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
option.baseDomain
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!form.watch("http") && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="protocol"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Protocol
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a protocol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="tcp">
|
||||||
|
TCP
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="udp">
|
||||||
|
UDP
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="proxyPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Port Number
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
field.onChange(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
? parseInt(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
The external
|
||||||
|
port number
|
||||||
|
to proxy
|
||||||
|
requests.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSnippets && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
Traefik: Add Entrypoints
|
||||||
|
</h3>
|
||||||
|
<CopyTextBox
|
||||||
|
text={`entryPoints:
|
||||||
|
${form.getValues("protocol")}-${form.getValues("proxyPort")}:
|
||||||
|
address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
Gerbil: Expose Ports in
|
||||||
|
Docker Compose
|
||||||
|
</h3>
|
||||||
|
<CopyTextBox
|
||||||
|
text={`ports:
|
||||||
|
- ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!form.watch("http") && (
|
|
||||||
<Link
|
<Link
|
||||||
className="text-sm text-primary flex items-center gap-1"
|
className="text-sm text-primary flex items-center gap-1"
|
||||||
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
||||||
|
@ -524,228 +774,15 @@ export default function CreateResourceForm({
|
||||||
</span>
|
</span>
|
||||||
<SquareArrowOutUpRight size={14} />
|
<SquareArrowOutUpRight size={14} />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
|
|
||||||
{!form.watch("http") && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="protocol"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Protocol
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={field.value}
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a protocol" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="tcp">
|
|
||||||
TCP
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="udp">
|
|
||||||
UDP
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
The protocol to use
|
|
||||||
for the resource.
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="proxyPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Port Number
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={
|
|
||||||
field.value ??
|
|
||||||
""
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target
|
|
||||||
.value
|
|
||||||
? parseInt(
|
|
||||||
e
|
|
||||||
.target
|
|
||||||
.value
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
The port number to
|
|
||||||
proxy requests to
|
|
||||||
(required for
|
|
||||||
non-HTTP resources).
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="siteId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col">
|
|
||||||
<FormLabel>Site</FormLabel>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<FormControl>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"justify-between",
|
|
||||||
!field.value &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.value
|
|
||||||
? sites.find(
|
|
||||||
(
|
|
||||||
site
|
|
||||||
) =>
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
)?.name
|
|
||||||
: "Select site"}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search site" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
No site
|
|
||||||
found.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map(
|
|
||||||
(
|
|
||||||
site
|
|
||||||
) => (
|
|
||||||
<CommandItem
|
|
||||||
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
|
||||||
key={
|
|
||||||
site.siteId
|
|
||||||
}
|
|
||||||
onSelect={() => {
|
|
||||||
form.setValue(
|
|
||||||
"siteId",
|
|
||||||
site.siteId
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
site.name
|
|
||||||
}
|
|
||||||
</CommandItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
This site will provide
|
|
||||||
connectivity to the
|
|
||||||
resource.
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showSnippets && (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
|
||||||
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
|
|
||||||
1
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow">
|
)}
|
||||||
<h3 className="text-lg font-semibold mb-3">
|
|
||||||
Traefik: Add Entrypoints
|
|
||||||
</h3>
|
|
||||||
<CopyTextBox
|
|
||||||
text={`entryPoints:
|
|
||||||
${form.getValues("protocol")}-${form.getValues("proxyPort")}:
|
|
||||||
address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
|
|
||||||
wrapText={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
|
||||||
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<h3 className="text-lg font-semibold mb-3">
|
|
||||||
Gerbil: Expose Ports in Docker
|
|
||||||
Compose
|
|
||||||
</h3>
|
|
||||||
<CopyTextBox
|
|
||||||
text={`ports:
|
|
||||||
- ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
|
|
||||||
wrapText={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className="text-sm text-primary flex items-center gap-1"
|
|
||||||
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Make sure to follow the full guide
|
|
||||||
</span>
|
|
||||||
<SquareArrowOutUpRight size={14} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
{!showSnippets && (
|
{!showSnippets && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -765,10 +802,6 @@ export default function CreateResourceForm({
|
||||||
Go to Resource
|
Go to Resource
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -233,7 +233,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
<Link
|
<Link
|
||||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outline"} className="ml-2">
|
<Button variant={"outlinePrimary"} className="ml-2">
|
||||||
Edit
|
Edit
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
@ -24,22 +24,22 @@ import {
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Resource } from "@server/db/schema";
|
import { Resource } from "@server/db/schema";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
const setPasswordFormSchema = z.object({
|
const setPasswordFormSchema = z.object({
|
||||||
password: z.string().min(4).max(100),
|
password: z.string().min(4).max(100)
|
||||||
});
|
});
|
||||||
|
|
||||||
type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
|
type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
|
||||||
|
|
||||||
const defaultValues: Partial<SetPasswordFormValues> = {
|
const defaultValues: Partial<SetPasswordFormValues> = {
|
||||||
password: "",
|
password: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
type SetPasswordFormProps = {
|
type SetPasswordFormProps = {
|
||||||
|
@ -53,7 +53,7 @@ export default function SetResourcePasswordForm({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
resourceId,
|
resourceId,
|
||||||
onSetPassword,
|
onSetPassword
|
||||||
}: SetPasswordFormProps) {
|
}: SetPasswordFormProps) {
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export default function SetResourcePasswordForm({
|
||||||
|
|
||||||
const form = useForm<SetPasswordFormValues>({
|
const form = useForm<SetPasswordFormValues>({
|
||||||
resolver: zodResolver(setPasswordFormSchema),
|
resolver: zodResolver(setPasswordFormSchema),
|
||||||
defaultValues,
|
defaultValues
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -76,7 +76,7 @@ export default function SetResourcePasswordForm({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
|
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
|
||||||
password: data.password,
|
password: data.password
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
|
@ -85,14 +85,14 @@ export default function SetResourcePasswordForm({
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while setting the resource password"
|
"An error occurred while setting the resource password"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: "Resource password set",
|
title: "Resource password set",
|
||||||
description:
|
description:
|
||||||
"The resource password has been set successfully",
|
"The resource password has been set successfully"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onSetPassword) {
|
if (onSetPassword) {
|
||||||
|
@ -153,6 +153,9 @@ export default function SetResourcePasswordForm({
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="set-password-form"
|
form="set-password-form"
|
||||||
|
@ -161,9 +164,6 @@ export default function SetResourcePasswordForm({
|
||||||
>
|
>
|
||||||
Enable Password Protection
|
Enable Password Protection
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
@ -24,27 +24,27 @@ import {
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { Resource } from "@server/db/schema";
|
import { Resource } from "@server/db/schema";
|
||||||
import {
|
import {
|
||||||
InputOTP,
|
InputOTP,
|
||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot
|
||||||
} from "@app/components/ui/input-otp";
|
} from "@app/components/ui/input-otp";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
const setPincodeFormSchema = z.object({
|
const setPincodeFormSchema = z.object({
|
||||||
pincode: z.string().length(6),
|
pincode: z.string().length(6)
|
||||||
});
|
});
|
||||||
|
|
||||||
type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
|
type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
|
||||||
|
|
||||||
const defaultValues: Partial<SetPincodeFormValues> = {
|
const defaultValues: Partial<SetPincodeFormValues> = {
|
||||||
pincode: "",
|
pincode: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
type SetPincodeFormProps = {
|
type SetPincodeFormProps = {
|
||||||
|
@ -58,7 +58,7 @@ export default function SetResourcePincodeForm({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
resourceId,
|
resourceId,
|
||||||
onSetPincode,
|
onSetPincode
|
||||||
}: SetPincodeFormProps) {
|
}: SetPincodeFormProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ export default function SetResourcePincodeForm({
|
||||||
|
|
||||||
const form = useForm<SetPincodeFormValues>({
|
const form = useForm<SetPincodeFormValues>({
|
||||||
resolver: zodResolver(setPincodeFormSchema),
|
resolver: zodResolver(setPincodeFormSchema),
|
||||||
defaultValues,
|
defaultValues
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -81,7 +81,7 @@ export default function SetResourcePincodeForm({
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
|
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
|
||||||
pincode: data.pincode,
|
pincode: data.pincode
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
|
@ -89,15 +89,15 @@ export default function SetResourcePincodeForm({
|
||||||
title: "Error setting resource PIN code",
|
title: "Error setting resource PIN code",
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
"An error occurred while setting the resource PIN code",
|
"An error occurred while setting the resource PIN code"
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: "Resource PIN code set",
|
title: "Resource PIN code set",
|
||||||
description:
|
description:
|
||||||
"The resource pincode has been set successfully",
|
"The resource pincode has been set successfully"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onSetPincode) {
|
if (onSetPincode) {
|
||||||
|
@ -181,6 +181,9 @@ export default function SetResourcePincodeForm({
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="set-pincode-form"
|
form="set-pincode-form"
|
||||||
|
@ -189,9 +192,6 @@ export default function SetResourcePincodeForm({
|
||||||
>
|
>
|
||||||
Enable PIN Code Protection
|
Enable PIN Code Protection
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -38,7 +38,8 @@ import {
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionFooter
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
@ -438,6 +439,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
setActiveRolesTagIndex
|
setActiveRolesTagIndex
|
||||||
}
|
}
|
||||||
placeholder="Select a role"
|
placeholder="Select a role"
|
||||||
|
size="sm"
|
||||||
tags={
|
tags={
|
||||||
usersRolesForm.getValues()
|
usersRolesForm.getValues()
|
||||||
.roles
|
.roles
|
||||||
|
@ -466,14 +468,6 @@ export default function ResourceAuthenticationPage() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
sortTags={true}
|
sortTags={true}
|
||||||
styleClasses={{
|
|
||||||
tag: {
|
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
|
||||||
},
|
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
|
||||||
inlineTagsContainer:
|
|
||||||
"bg-transparent p-2"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -504,6 +498,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
usersRolesForm.getValues()
|
usersRolesForm.getValues()
|
||||||
.users
|
.users
|
||||||
}
|
}
|
||||||
|
size="sm"
|
||||||
setTags={(
|
setTags={(
|
||||||
newUsers
|
newUsers
|
||||||
) => {
|
) => {
|
||||||
|
@ -528,14 +523,6 @@ export default function ResourceAuthenticationPage() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
sortTags={true}
|
sortTags={true}
|
||||||
styleClasses={{
|
|
||||||
tag: {
|
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
|
||||||
},
|
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
|
||||||
inlineTagsContainer:
|
|
||||||
"bg-transparent p-2"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
@ -582,7 +569,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outlinePrimary"
|
||||||
onClick={
|
onClick={
|
||||||
authInfo.password
|
authInfo.password
|
||||||
? removeResourcePassword
|
? removeResourcePassword
|
||||||
|
@ -608,7 +595,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outlinePrimary"
|
||||||
onClick={
|
onClick={
|
||||||
authInfo.pincode
|
authInfo.pincode
|
||||||
? removeResourcePincode
|
? removeResourcePincode
|
||||||
|
@ -664,6 +651,7 @@ export default function ResourceAuthenticationPage() {
|
||||||
activeTagIndex={
|
activeTagIndex={
|
||||||
activeEmailTagIndex
|
activeEmailTagIndex
|
||||||
}
|
}
|
||||||
|
size={"sm"}
|
||||||
validateTag={(
|
validateTag={(
|
||||||
tag
|
tag
|
||||||
) => {
|
) => {
|
||||||
|
@ -708,14 +696,6 @@ export default function ResourceAuthenticationPage() {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
sortTags={true}
|
sortTags={true}
|
||||||
styleClasses={{
|
|
||||||
tag: {
|
|
||||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
|
||||||
},
|
|
||||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
|
||||||
inlineTagsContainer:
|
|
||||||
"bg-transparent p-2"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
|
@ -241,10 +242,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
>(`/resource/${params.resourceId}/target`, data);
|
>(`/resource/${params.resourceId}/target`, data);
|
||||||
target.targetId = res.data.data.targetId;
|
target.targetId = res.data.data.targetId;
|
||||||
} else if (target.updated) {
|
} else if (target.updated) {
|
||||||
await api.post(
|
await api.post(`/target/${target.targetId}`, data);
|
||||||
`/target/${target.targetId}`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTargets([
|
setTargets([
|
||||||
|
@ -261,9 +259,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
|
|
||||||
for (const targetId of targetsToRemove) {
|
for (const targetId of targetsToRemove) {
|
||||||
await api.delete(`/target/${targetId}`);
|
await api.delete(`/target/${targetId}`);
|
||||||
setTargets(
|
setTargets(targets.filter((t) => t.targetId !== targetId));
|
||||||
targets.filter((t) => t.targetId !== targetId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
@ -459,7 +455,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
SSL Configuration
|
SSL Configuration
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
Setup SSL to secure your connections with Let's Encrypt certificates
|
Setup SSL to secure your connections with Let's
|
||||||
|
Encrypt certificates
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
|
@ -490,7 +487,7 @@ export default function ReverseProxyTargets(props: {
|
||||||
onSubmit={addTargetForm.handleSubmit(addTarget)}
|
onSubmit={addTargetForm.handleSubmit(addTarget)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-start">
|
||||||
{resource.http && (
|
{resource.http && (
|
||||||
<FormField
|
<FormField
|
||||||
control={addTargetForm.control}
|
control={addTargetForm.control}
|
||||||
|
@ -545,18 +542,6 @@ export default function ReverseProxyTargets(props: {
|
||||||
<Input id="ip" {...field} />
|
<Input id="ip" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{site?.type === "newt" ? (
|
|
||||||
<FormDescription>
|
|
||||||
This is the IP or hostname
|
|
||||||
of the target service on
|
|
||||||
your network.
|
|
||||||
</FormDescription>
|
|
||||||
) : site?.type === "wireguard" ? (
|
|
||||||
<FormDescription>
|
|
||||||
This is the IP of the
|
|
||||||
WireGuard peer.
|
|
||||||
</FormDescription>
|
|
||||||
) : null}
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -575,83 +560,68 @@ export default function ReverseProxyTargets(props: {
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{site?.type === "newt" ? (
|
|
||||||
<FormDescription>
|
|
||||||
This is the port of the
|
|
||||||
target service on your
|
|
||||||
network.
|
|
||||||
</FormDescription>
|
|
||||||
) : site?.type === "wireguard" ? (
|
|
||||||
<FormDescription>
|
|
||||||
This is the port exposed on
|
|
||||||
an address on the WireGuard
|
|
||||||
network.
|
|
||||||
</FormDescription>
|
|
||||||
) : null}
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlinePrimary"
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
Add Target
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" variant="outline">
|
|
||||||
Add Target
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<TableContainer>
|
<Table>
|
||||||
<Table>
|
<TableHeader>
|
||||||
<TableHeader>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableRow key={headerGroup.id}>
|
||||||
<TableRow key={headerGroup.id}>
|
{headerGroup.headers.map((header) => (
|
||||||
{headerGroup.headers.map((header) => (
|
<TableHead key={header.id}>
|
||||||
<TableHead key={header.id}>
|
{header.isPlaceholder
|
||||||
{header.isPlaceholder
|
? null
|
||||||
? null
|
: flexRender(
|
||||||
: flexRender(
|
header.column.columnDef
|
||||||
header.column
|
.header,
|
||||||
.columnDef.header,
|
header.getContext()
|
||||||
header.getContext()
|
)}
|
||||||
)}
|
</TableHead>
|
||||||
</TableHead>
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</TableHeader>
|
) : (
|
||||||
<TableBody>
|
<TableRow>
|
||||||
{table.getRowModel().rows?.length ? (
|
<TableCell
|
||||||
table.getRowModel().rows.map((row) => (
|
colSpan={columns.length}
|
||||||
<TableRow key={row.id}>
|
className="h-24 text-center"
|
||||||
{row
|
>
|
||||||
.getVisibleCells()
|
No targets. Add a target using the form.
|
||||||
.map((cell) => (
|
</TableCell>
|
||||||
<TableCell key={cell.id}>
|
</TableRow>
|
||||||
{flexRender(
|
)}
|
||||||
cell.column
|
</TableBody>
|
||||||
.columnDef.cell,
|
<TableCaption>
|
||||||
cell.getContext()
|
Adding more than one target above will enable load
|
||||||
)}
|
balancing.
|
||||||
</TableCell>
|
</TableCaption>
|
||||||
))}
|
</Table>
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
No targets. Add a target using the
|
|
||||||
form.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Adding more than one target above will enable load
|
|
||||||
balancing.
|
|
||||||
</p>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -129,6 +129,7 @@ export default function GeneralForm() {
|
||||||
ListDomainsResponse["domains"]
|
ListDomainsResponse["domains"]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
||||||
resource.isBaseDomain ? "basedomain" : "subdomain"
|
resource.isBaseDomain ? "basedomain" : "subdomain"
|
||||||
);
|
);
|
||||||
|
@ -184,8 +185,14 @@ export default function GeneralForm() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchDomains();
|
const load = async () => {
|
||||||
fetchSites();
|
await fetchDomains();
|
||||||
|
await fetchSites();
|
||||||
|
|
||||||
|
setLoadingPage(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function onSubmit(data: GeneralFormValues) {
|
async function onSubmit(data: GeneralFormValues) {
|
||||||
|
@ -263,391 +270,406 @@ export default function GeneralForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
!loadingPage && (
|
||||||
<SettingsSection>
|
<SettingsContainer>
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
General Settings
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
General Settings
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
Configure the general settings for this resource
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
Configure the general settings for this resource
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...form} key={formKey}>
|
<Form {...form} key={formKey}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-4"
|
className="grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||||
id="general-settings-form"
|
id="general-settings-form"
|
||||||
>
|
>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
This is the display name of the
|
|
||||||
resource.
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{resource.http && (
|
|
||||||
<>
|
|
||||||
{env.flags.allowBaseDomainResources && (
|
|
||||||
<div>
|
|
||||||
<RadioGroup
|
|
||||||
className="flex space-x-4"
|
|
||||||
defaultValue={domainType}
|
|
||||||
onValueChange={(val) => {
|
|
||||||
setDomainType(
|
|
||||||
val as any
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"isBaseDomain",
|
|
||||||
val === "basedomain"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="subdomain"
|
|
||||||
id="r1"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="r1">
|
|
||||||
Subdomain
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem
|
|
||||||
value="basedomain"
|
|
||||||
id="r2"
|
|
||||||
/>
|
|
||||||
<Label htmlFor="r2">
|
|
||||||
Base Domain
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{domainType === "subdomain" ? (
|
|
||||||
<div className="w-fill space-y-2">
|
|
||||||
{!env.flags
|
|
||||||
.allowBaseDomainResources && (
|
|
||||||
<FormLabel>
|
|
||||||
Subdomain
|
|
||||||
</FormLabel>
|
|
||||||
)}
|
|
||||||
<div className="flex">
|
|
||||||
<div className="w-full mr-1">
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
form.control
|
|
||||||
}
|
|
||||||
name="subdomain"
|
|
||||||
render={({
|
|
||||||
field
|
|
||||||
}) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
className="text-right"
|
|
||||||
placeholder="Enter subdomain"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-1/2">
|
|
||||||
<FormField
|
|
||||||
control={
|
|
||||||
form.control
|
|
||||||
}
|
|
||||||
name="domainId"
|
|
||||||
render={({
|
|
||||||
field
|
|
||||||
}) => (
|
|
||||||
<FormItem>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
field.value
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{baseDomains.map(
|
|
||||||
(
|
|
||||||
option
|
|
||||||
) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
>
|
|
||||||
.
|
|
||||||
{
|
|
||||||
option.baseDomain
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="domainId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<Select
|
|
||||||
onValueChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value ||
|
|
||||||
baseDomains[0]
|
|
||||||
?.domainId
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{baseDomains.map(
|
|
||||||
(
|
|
||||||
option
|
|
||||||
) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
option.domainId
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
option.baseDomain
|
|
||||||
}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!resource.http && (
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="proxyPort"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
Port Number
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
type="number"
|
|
||||||
value={
|
|
||||||
field.value ?? ""
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target.value
|
|
||||||
? parseInt(
|
|
||||||
e
|
|
||||||
.target
|
|
||||||
.value
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
|
||||||
This is the port that will
|
|
||||||
be used to access the
|
|
||||||
resource.
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
{resource.http && (
|
||||||
<Button
|
<>
|
||||||
type="submit"
|
{env.flags
|
||||||
loading={saveLoading}
|
.allowBaseDomainResources && (
|
||||||
disabled={saveLoading}
|
<FormField
|
||||||
form="general-settings-form"
|
control={form.control}
|
||||||
>
|
name="isBaseDomain"
|
||||||
Save Settings
|
render={({ field }) => (
|
||||||
</Button>
|
<FormItem>
|
||||||
</SettingsSectionFooter>
|
<FormLabel>
|
||||||
</SettingsSection>
|
Domain Type
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
domainType
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
val
|
||||||
|
) => {
|
||||||
|
setDomainType(
|
||||||
|
val ===
|
||||||
|
"basedomain"
|
||||||
|
? "basedomain"
|
||||||
|
: "subdomain"
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"isBaseDomain",
|
||||||
|
val ===
|
||||||
|
"basedomain"
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="subdomain">
|
||||||
|
Subdomain
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="basedomain">
|
||||||
|
Base
|
||||||
|
Domain
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingsSection>
|
<div className="col-span-2">
|
||||||
<SettingsSectionHeader>
|
{domainType === "subdomain" ? (
|
||||||
<SettingsSectionTitle>
|
<div className="w-fill space-y-2">
|
||||||
Transfer Resource
|
<FormLabel>
|
||||||
</SettingsSectionTitle>
|
Subdomain
|
||||||
<SettingsSectionDescription>
|
</FormLabel>
|
||||||
Transfer this resource to a different site
|
<div className="flex">
|
||||||
</SettingsSectionDescription>
|
<div className="w-1/2">
|
||||||
</SettingsSectionHeader>
|
<FormField
|
||||||
|
control={
|
||||||
<SettingsSectionBody>
|
form.control
|
||||||
<SettingsSectionForm>
|
}
|
||||||
<Form {...transferForm}>
|
name="subdomain"
|
||||||
<form
|
render={({
|
||||||
onSubmit={transferForm.handleSubmit(onTransfer)}
|
field
|
||||||
className="space-y-4"
|
}) => (
|
||||||
id="transfer-form"
|
<FormItem>
|
||||||
>
|
<FormControl>
|
||||||
<FormField
|
<Input
|
||||||
control={transferForm.control}
|
{...field}
|
||||||
name="siteId"
|
className="border-r-0 rounded-r-none"
|
||||||
render={({ field }) => (
|
/>
|
||||||
<FormItem>
|
</FormControl>
|
||||||
<FormLabel>
|
<FormMessage />
|
||||||
Destination Site
|
</FormItem>
|
||||||
</FormLabel>
|
)}
|
||||||
<Popover
|
/>
|
||||||
open={open}
|
</div>
|
||||||
onOpenChange={setOpen}
|
<div className="w-1/2">
|
||||||
>
|
<FormField
|
||||||
<PopoverTrigger asChild>
|
control={
|
||||||
<FormControl>
|
form.control
|
||||||
<Button
|
}
|
||||||
variant="outline"
|
name="domainId"
|
||||||
role="combobox"
|
render={({
|
||||||
className={cn(
|
field
|
||||||
"w-full justify-between",
|
}) => (
|
||||||
!field.value &&
|
<FormItem>
|
||||||
"text-muted-foreground"
|
<Select
|
||||||
)}
|
onValueChange={
|
||||||
>
|
field.onChange
|
||||||
{field.value
|
}
|
||||||
? sites.find(
|
defaultValue={
|
||||||
(site) =>
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
)?.name
|
|
||||||
: "Select site"}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-full p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder="Search sites"
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
<CommandEmpty>
|
|
||||||
No sites found.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map(
|
|
||||||
(site) => (
|
|
||||||
<CommandItem
|
|
||||||
value={`${site.name}:${site.siteId}`}
|
|
||||||
key={
|
|
||||||
site.siteId
|
|
||||||
}
|
|
||||||
onSelect={() => {
|
|
||||||
transferForm.setValue(
|
|
||||||
"siteId",
|
|
||||||
site.siteId
|
|
||||||
);
|
|
||||||
setOpen(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
site.name
|
|
||||||
}
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"ml-auto h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
field.value
|
||||||
? "opacity-100"
|
}
|
||||||
: "opacity-0"
|
value={
|
||||||
)}
|
field.value
|
||||||
/>
|
}
|
||||||
</CommandItem>
|
>
|
||||||
)
|
<FormControl>
|
||||||
)}
|
<SelectTrigger className="rounded-l-none">
|
||||||
</CommandGroup>
|
<SelectValue />
|
||||||
</Command>
|
</SelectTrigger>
|
||||||
</PopoverContent>
|
</FormControl>
|
||||||
</Popover>
|
<SelectContent>
|
||||||
<FormMessage />
|
{baseDomains.map(
|
||||||
</FormItem>
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
.
|
||||||
|
{
|
||||||
|
option.baseDomain
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="domainId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Base Domain
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
defaultValue={
|
||||||
|
field.value ||
|
||||||
|
baseDomains[0]
|
||||||
|
?.domainId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{baseDomains.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.domainId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
option.baseDomain
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
{!resource.http && (
|
||||||
<Button
|
<FormField
|
||||||
type="submit"
|
control={form.control}
|
||||||
loading={transferLoading}
|
name="proxyPort"
|
||||||
disabled={transferLoading}
|
render={({ field }) => (
|
||||||
form="transfer-form"
|
<FormItem>
|
||||||
>
|
<FormLabel>
|
||||||
Transfer Resource
|
Port Number
|
||||||
</Button>
|
</FormLabel>
|
||||||
</SettingsSectionFooter>
|
<FormControl>
|
||||||
</SettingsSection>
|
<Input
|
||||||
</SettingsContainer>
|
type="number"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
? parseInt(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={saveLoading}
|
||||||
|
disabled={saveLoading}
|
||||||
|
form="general-settings-form"
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
Transfer Resource
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
Transfer this resource to a different site
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<Form {...transferForm}>
|
||||||
|
<form
|
||||||
|
onSubmit={transferForm.handleSubmit(
|
||||||
|
onTransfer
|
||||||
|
)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="transfer-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={transferForm.control}
|
||||||
|
name="siteId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Destination Site
|
||||||
|
</FormLabel>
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-between",
|
||||||
|
!field.value &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? sites.find(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) =>
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
)?.name
|
||||||
|
: "Select site"}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-full p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search sites"
|
||||||
|
/>
|
||||||
|
<CommandEmpty>
|
||||||
|
No sites found.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{sites.map(
|
||||||
|
(site) => (
|
||||||
|
<CommandItem
|
||||||
|
value={`${site.name}:${site.siteId}`}
|
||||||
|
key={
|
||||||
|
site.siteId
|
||||||
|
}
|
||||||
|
onSelect={() => {
|
||||||
|
transferForm.setValue(
|
||||||
|
"siteId",
|
||||||
|
site.siteId
|
||||||
|
);
|
||||||
|
setOpen(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
site.name
|
||||||
|
}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
loading={transferLoading}
|
||||||
|
disabled={transferLoading}
|
||||||
|
form="transfer-form"
|
||||||
|
>
|
||||||
|
Transfer Resource
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
</SettingsContainer>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,9 +130,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||||
<OrgProvider org={org}>
|
<OrgProvider org={org}>
|
||||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||||
<div className="mb-8">
|
<ResourceInfoBox />
|
||||||
<ResourceInfoBox />
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</SidebarSettings>
|
</SidebarSettings>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
|
@ -94,7 +95,7 @@ enum RuleAction {
|
||||||
enum RuleMatch {
|
enum RuleMatch {
|
||||||
PATH = "Path",
|
PATH = "Path",
|
||||||
IP = "IP",
|
IP = "IP",
|
||||||
CIDR = "IP Range",
|
CIDR = "IP Range"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ResourceRules(props: {
|
export default function ResourceRules(props: {
|
||||||
|
@ -623,7 +624,7 @@ export default function ResourceRules(props: {
|
||||||
onSubmit={addRuleForm.handleSubmit(addRule)}
|
onSubmit={addRuleForm.handleSubmit(addRule)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 items-end">
|
||||||
<FormField
|
<FormField
|
||||||
control={addRuleForm.control}
|
control={addRuleForm.control}
|
||||||
name="action"
|
name="action"
|
||||||
|
@ -711,68 +712,63 @@ export default function ResourceRules(props: {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="outlinePrimary"
|
||||||
|
disabled={!rulesEnabled}
|
||||||
|
>
|
||||||
|
Add Rule
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="outline"
|
|
||||||
disabled={!rulesEnabled}
|
|
||||||
>
|
|
||||||
Add Rule
|
|
||||||
</Button>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
<TableContainer>
|
<Table>
|
||||||
<Table>
|
<TableHeader>
|
||||||
<TableHeader>
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
<TableRow key={headerGroup.id}>
|
||||||
<TableRow key={headerGroup.id}>
|
{headerGroup.headers.map((header) => (
|
||||||
{headerGroup.headers.map((header) => (
|
<TableHead key={header.id}>
|
||||||
<TableHead key={header.id}>
|
{header.isPlaceholder
|
||||||
{header.isPlaceholder
|
? null
|
||||||
? null
|
: flexRender(
|
||||||
: flexRender(
|
header.column.columnDef
|
||||||
header.column
|
.header,
|
||||||
.columnDef.header,
|
header.getContext()
|
||||||
header.getContext()
|
)}
|
||||||
)}
|
</TableHead>
|
||||||
</TableHead>
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))
|
||||||
</TableHeader>
|
) : (
|
||||||
<TableBody>
|
<TableRow>
|
||||||
{table.getRowModel().rows?.length ? (
|
<TableCell
|
||||||
table.getRowModel().rows.map((row) => (
|
colSpan={columns.length}
|
||||||
<TableRow key={row.id}>
|
className="h-24 text-center"
|
||||||
{row
|
>
|
||||||
.getVisibleCells()
|
No rules. Add a rule using the form.
|
||||||
.map((cell) => (
|
</TableCell>
|
||||||
<TableCell key={cell.id}>
|
</TableRow>
|
||||||
{flexRender(
|
)}
|
||||||
cell.column
|
</TableBody>
|
||||||
.columnDef.cell,
|
<TableCaption>
|
||||||
cell.getContext()
|
Rules are evaluated by priority in ascending order.
|
||||||
)}
|
</TableCaption>
|
||||||
</TableCell>
|
</Table>
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
No rules. Add a rule using the form.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Rules are evaluated by priority in ascending order.
|
|
||||||
</p>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
<SettingsSectionFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -152,13 +152,15 @@ export default function CreateShareLinkForm({
|
||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
setResources(
|
setResources(
|
||||||
res.data.data.resources.filter((r) => {
|
res.data.data.resources
|
||||||
return r.http;
|
.filter((r) => {
|
||||||
}).map((r) => ({
|
return r.http;
|
||||||
resourceId: r.resourceId,
|
})
|
||||||
name: r.name,
|
.map((r) => ({
|
||||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
|
resourceId: r.resourceId,
|
||||||
}))
|
name: r.name,
|
||||||
|
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,7 +276,7 @@ export default function CreateShareLinkForm({
|
||||||
name="resourceId"
|
name="resourceId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel className="mb-2">
|
<FormLabel>
|
||||||
Resource
|
Resource
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Popover>
|
<Popover>
|
||||||
|
@ -318,9 +320,7 @@ export default function CreateShareLinkForm({
|
||||||
r
|
r
|
||||||
) => (
|
) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={
|
value={`${r.name}:${r.resourceId}`}
|
||||||
`${r.name}:${r.resourceId}`
|
|
||||||
}
|
|
||||||
key={
|
key={
|
||||||
r.resourceId
|
r.resourceId
|
||||||
}
|
}
|
||||||
|
@ -369,13 +369,11 @@ export default function CreateShareLinkForm({
|
||||||
name="title"
|
name="title"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Label>
|
<FormLabel>
|
||||||
Title (optional)
|
Title (optional)
|
||||||
</Label>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -383,66 +381,68 @@ export default function CreateShareLinkForm({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label>Expire In</Label>
|
<div className="space-y-2">
|
||||||
<div className="grid grid-cols-2 gap-4 mt-2">
|
<FormLabel>Expire In</FormLabel>
|
||||||
<FormField
|
<div className="grid grid-cols-2 gap-4">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="timeUnit"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="timeUnit"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<Select
|
<FormItem>
|
||||||
onValueChange={
|
<Select
|
||||||
field.onChange
|
onValueChange={
|
||||||
}
|
field.onChange
|
||||||
defaultValue={field.value.toString()}
|
}
|
||||||
>
|
defaultValue={field.value.toString()}
|
||||||
<FormControl>
|
>
|
||||||
<SelectTrigger>
|
<FormControl>
|
||||||
<SelectValue placeholder="Select duration" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select duration" />
|
||||||
</FormControl>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
</FormControl>
|
||||||
{timeUnits.map(
|
<SelectContent>
|
||||||
(
|
{timeUnits.map(
|
||||||
option
|
(
|
||||||
) => (
|
option
|
||||||
<SelectItem
|
) => (
|
||||||
key={
|
<SelectItem
|
||||||
option.unit
|
key={
|
||||||
}
|
option.unit
|
||||||
value={
|
}
|
||||||
option.unit
|
value={
|
||||||
}
|
option.unit
|
||||||
>
|
}
|
||||||
{
|
>
|
||||||
option.name
|
{
|
||||||
}
|
option.name
|
||||||
</SelectItem>
|
}
|
||||||
)
|
</SelectItem>
|
||||||
)}
|
)
|
||||||
</SelectContent>
|
)}
|
||||||
</Select>
|
</SelectContent>
|
||||||
<FormMessage />
|
</Select>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="timeValue"
|
name="timeValue"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
@ -552,6 +552,9 @@ export default function CreateShareLinkForm({
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={form.handleSubmit(onSubmit)}
|
onClick={form.handleSubmit(onSubmit)}
|
||||||
|
@ -560,9 +563,6 @@ export default function CreateShareLinkForm({
|
||||||
>
|
>
|
||||||
Create Link
|
Create Link
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -273,7 +273,21 @@ export default function ShareLinksTable({
|
||||||
}
|
}
|
||||||
return "Never";
|
return "Never";
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "delete",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center justify-end space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outlinePrimary"
|
||||||
|
onClick={() => deleteSharelink(row.original.accessTokenId)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -41,6 +41,7 @@ import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
Loader2,
|
||||||
SquareArrowOutUpRight
|
SquareArrowOutUpRight
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
@ -48,6 +49,7 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger
|
CollapsibleTrigger
|
||||||
} from "@app/components/ui/collapsible";
|
} from "@app/components/ui/collapsible";
|
||||||
|
import LoaderPlaceholder from "@app/components/PlaceHolderLoader";
|
||||||
|
|
||||||
const createSiteFormSchema = z.object({
|
const createSiteFormSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
|
@ -97,6 +99,8 @@ export default function CreateSiteForm({
|
||||||
const [siteDefaults, setSiteDefaults] =
|
const [siteDefaults, setSiteDefaults] =
|
||||||
useState<PickSiteDefaultsResponse | null>(null);
|
useState<PickSiteDefaultsResponse | null>(null);
|
||||||
|
|
||||||
|
const [loadingPage, setLoadingPage] = useState(true);
|
||||||
|
|
||||||
const handleCheckboxChange = (checked: boolean) => {
|
const handleCheckboxChange = (checked: boolean) => {
|
||||||
// setChecked?.(checked);
|
// setChecked?.(checked);
|
||||||
setIsChecked(checked);
|
setIsChecked(checked);
|
||||||
|
@ -121,27 +125,36 @@ export default function CreateSiteForm({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
// reset all values
|
const load = async () => {
|
||||||
setLoading?.(false);
|
setLoadingPage(true);
|
||||||
setIsLoading(false);
|
// reset all values
|
||||||
form.reset();
|
setLoading?.(false);
|
||||||
setChecked?.(false);
|
setIsLoading(false);
|
||||||
setKeypair(null);
|
form.reset();
|
||||||
setSiteDefaults(null);
|
setChecked?.(false);
|
||||||
|
setKeypair(null);
|
||||||
|
setSiteDefaults(null);
|
||||||
|
|
||||||
const generatedKeypair = generateKeypair();
|
const generatedKeypair = generateKeypair();
|
||||||
setKeypair(generatedKeypair);
|
setKeypair(generatedKeypair);
|
||||||
|
|
||||||
api.get(`/org/${orgId}/pick-site-defaults`)
|
await api
|
||||||
.catch((e) => {
|
.get(`/org/${orgId}/pick-site-defaults`)
|
||||||
// update the default value of the form to be local method
|
.catch((e) => {
|
||||||
form.setValue("method", "local");
|
// update the default value of the form to be local method
|
||||||
})
|
form.setValue("method", "local");
|
||||||
.then((res) => {
|
})
|
||||||
if (res && res.status === 200) {
|
.then((res) => {
|
||||||
setSiteDefaults(res.data.data);
|
if (res && res.status === 200) {
|
||||||
}
|
setSiteDefaults(res.data.data);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
setLoadingPage(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
async function onSubmit(data: CreateSiteFormValues) {
|
async function onSubmit(data: CreateSiteFormValues) {
|
||||||
|
@ -257,7 +270,9 @@ PersistentKeepalive = 5`
|
||||||
|
|
||||||
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||||
|
|
||||||
return (
|
return loadingPage ? (
|
||||||
|
<LoaderPlaceholder height="300px" />
|
||||||
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
@ -276,8 +291,7 @@ PersistentKeepalive = 5`
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This is the the display name for the
|
This is the the display name for the site.
|
||||||
site.
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
@ -331,7 +345,6 @@ PersistentKeepalive = 5`
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{" "}
|
|
||||||
Learn how to install Newt on your system
|
Learn how to install Newt on your system
|
||||||
</span>
|
</span>
|
||||||
<SquareArrowOutUpRight size={14} />
|
<SquareArrowOutUpRight size={14} />
|
||||||
|
@ -358,12 +371,16 @@ PersistentKeepalive = 5`
|
||||||
onOpenChange={setIsOpen}
|
onOpenChange={setIsOpen}
|
||||||
className="space-y-2"
|
className="space-y-2"
|
||||||
>
|
>
|
||||||
<div className="mx-auto">
|
<div className="mx-auto mb-2">
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={newtConfig}
|
text={newtConfig}
|
||||||
wrapText={false}
|
wrapText={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
You will only be able to see the
|
||||||
|
configuration once.
|
||||||
|
</span>
|
||||||
<div className="flex items-center justify-between space-x-4">
|
<div className="flex items-center justify-between space-x-4">
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
@ -405,10 +422,6 @@ PersistentKeepalive = 5`
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
You will only be able to see the
|
|
||||||
configuration once.
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,6 +58,9 @@ export default function CreateSiteFormModal({
|
||||||
</div>
|
</div>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="create-site-form"
|
form="create-site-form"
|
||||||
|
@ -69,9 +72,6 @@ export default function CreateSiteFormModal({
|
||||||
>
|
>
|
||||||
Create Site
|
Create Site
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -268,7 +268,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
<Link
|
<Link
|
||||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||||
>
|
>
|
||||||
<Button variant={"outline"} className="ml-2">
|
<Button variant={"outlinePrimary"} className="ml-2">
|
||||||
Edit
|
Edit
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -68,9 +68,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
|
|
||||||
<SiteProvider site={site}>
|
<SiteProvider site={site}>
|
||||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||||
<div className="mb-8">
|
<SiteInfoCard />
|
||||||
<SiteInfoCard />
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</SidebarSettings>
|
</SidebarSettings>
|
||||||
</SiteProvider>
|
</SiteProvider>
|
||||||
|
|
|
@ -198,8 +198,7 @@ export default function VerifyEmailForm({
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
We sent a verification code to your
|
We sent a verification code to your
|
||||||
email address. Please enter the code
|
email address.
|
||||||
to verify your email address.
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -21,8 +21,8 @@
|
||||||
--accent-foreground: 24 9.8% 10%;
|
--accent-foreground: 24 9.8% 10%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
--border: 20 5.9% 85%;
|
--border: 20 5.9% 80%;
|
||||||
--input: 20 5.9% 85%;
|
--input: 20 5.9% 75%;
|
||||||
--ring: 24.6 95% 53.1%;
|
--ring: 24.6 95% 53.1%;
|
||||||
--radius: 0.75rem;
|
--radius: 0.75rem;
|
||||||
--chart-1: 12 76% 61%;
|
--chart-1: 12 76% 61%;
|
||||||
|
@ -49,8 +49,8 @@
|
||||||
--accent-foreground: 60 9.1% 97.8%;
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
--destructive: 0 72.2% 50.6%;
|
--destructive: 0 72.2% 50.6%;
|
||||||
--destructive-foreground: 60 9.1% 97.8%;
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
--border: 12 6.5% 25.0%;
|
--border: 12 6.5% 30.0%;
|
||||||
--input: 12 6.5% 25.0%;
|
--input: 12 6.5% 35.0%;
|
||||||
--ring: 20.5 90.2% 48.2%;
|
--ring: 20.5 90.2% 48.2%;
|
||||||
--chart-1: 220 70% 50%;
|
--chart-1: 220 70% 50%;
|
||||||
--chart-2: 160 60% 45%;
|
--chart-2: 160 60% 45%;
|
||||||
|
|
|
@ -37,11 +37,11 @@ export default async function RootLayout({
|
||||||
>
|
>
|
||||||
<EnvProvider env={pullEnv()}>
|
<EnvProvider env={pullEnv()}>
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex-grow">{children}</div>
|
<div className="flex-grow pb-3 md:pb-0">{children}</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="w-full mt-12 py-3 mb-6 px-4">
|
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
|
||||||
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600 select-none">
|
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||||
<span>Pangolin</span>
|
<span>Pangolin</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import {
|
import {
|
||||||
|
@ -15,14 +15,14 @@ import {
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
import { useToast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import {
|
||||||
InviteUserBody,
|
InviteUserBody,
|
||||||
InviteUserResponse,
|
InviteUserResponse,
|
||||||
ListUsersResponse,
|
ListUsersResponse
|
||||||
} from "@server/routers/user";
|
} from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
@ -37,7 +37,7 @@ import {
|
||||||
CredenzaDescription,
|
CredenzaDescription,
|
||||||
CredenzaFooter,
|
CredenzaFooter,
|
||||||
CredenzaHeader,
|
CredenzaHeader,
|
||||||
CredenzaTitle,
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { Description } from "@radix-ui/react-toast";
|
import { Description } from "@radix-ui/react-toast";
|
||||||
|
@ -61,7 +61,7 @@ export default function InviteUserForm({
|
||||||
title,
|
title,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
buttonText,
|
buttonText,
|
||||||
dialog,
|
dialog
|
||||||
}: InviteUserFormProps) {
|
}: InviteUserFormProps) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
@ -69,15 +69,15 @@ export default function InviteUserForm({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
string: z.string().refine((val) => val === string, {
|
string: z.string().refine((val) => val === string, {
|
||||||
message: "Invalid confirmation",
|
message: "Invalid confirmation"
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
string: "",
|
string: ""
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
@ -128,6 +128,9 @@ export default function InviteUserForm({
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="confirm-delete-form"
|
form="confirm-delete-form"
|
||||||
|
@ -136,9 +139,6 @@ export default function InviteUserForm({
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -78,7 +78,7 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
|
<CredenzaClose className={cn("mb-3 mt-3 md:mt-0 md:mb-0", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
);
|
);
|
||||||
|
@ -155,7 +155,7 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
// );
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("px-0 mb-4", className)} {...props}>
|
<div className={cn("px-0 mb-4 space-y-4", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -168,7 +168,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CredenzaFooter className={className} {...props}>
|
<CredenzaFooter className={cn("mt-8 md:mt-0", className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
);
|
);
|
||||||
|
|
|
@ -29,10 +29,8 @@ import {
|
||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { InputOTP, InputOTPGroup, InputOTPSlot } from "./ui/input-otp";
|
|
||||||
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
|
|
||||||
import { CheckCircle2 } from "lucide-react";
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
|
||||||
const disableSchema = z.object({
|
const disableSchema = z.object({
|
||||||
|
@ -152,36 +150,7 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||||
Authenticator Code
|
Authenticator Code
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<InputOTP
|
<Input {...field} />
|
||||||
maxLength={6}
|
|
||||||
{...field}
|
|
||||||
pattern={
|
|
||||||
REGEXP_ONLY_DIGITS_AND_CHARS
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={0}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={1}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={2}
|
|
||||||
/>
|
|
||||||
</InputOTPGroup>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={3}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={4}
|
|
||||||
/>
|
|
||||||
<InputOTPSlot
|
|
||||||
index={5}
|
|
||||||
/>
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -210,6 +179,9 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||||
)}
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
{step === "password" && (
|
{step === "password" && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -220,9 +192,6 @@ export default function Disable2FaForm({ open, setOpen }: Disable2FaProps) {
|
||||||
Disable 2FA
|
Disable 2FA
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { formatAxiosError } from "@app/lib/api";;
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
|
@ -222,7 +222,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
||||||
<QRCodeCanvas value={secretUri} size={200} />
|
<QRCodeCanvas value={secretUri} size={200} />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-md mx-auto">
|
<div className="max-w-md mx-auto">
|
||||||
<CopyTextBox text={secretUri} wrapText={false} />
|
<CopyTextBox
|
||||||
|
text={secretUri}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Form {...confirmForm}>
|
<Form {...confirmForm}>
|
||||||
|
@ -279,6 +282,9 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
||||||
)}
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
{(step === 1 || step === 2) && (
|
{(step === 1 || step === 2) && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -295,9 +301,6 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Close</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
</CredenzaFooter>
|
</CredenzaFooter>
|
||||||
</CredenzaContent>
|
</CredenzaContent>
|
||||||
</Credenza>
|
</Credenza>
|
||||||
|
|
21
src/components/PlaceHolderLoader.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Loader2 } from "lucide-react"; // Ensure you have lucide-react installed
|
||||||
|
|
||||||
|
interface LoaderProps {
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoaderPlaceholder: React.FC<LoaderProps> = ({ height = "100px" }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center w-full"
|
||||||
|
style={{ height }}
|
||||||
|
>
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoaderPlaceholder;
|
|
@ -1,5 +1,5 @@
|
||||||
export function SettingsContainer({ children }: { children: React.ReactNode }) {
|
export function SettingsContainer({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="space-y-4">{children}</div>
|
return <div className="space-y-6">{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSection({ children }: { children: React.ReactNode }) {
|
export function SettingsSection({ children }: { children: React.ReactNode }) {
|
||||||
|
@ -7,7 +7,7 @@ export function SettingsSection({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSectionHeader({ children }: { children: React.ReactNode }) {
|
export function SettingsSectionHeader({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="space-y-0.5 pb-8">{children}</div>
|
return <div className="space-y-0.5 pb-6">{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSectionForm({ children }: { children: React.ReactNode }) {
|
export function SettingsSectionForm({ children }: { children: React.ReactNode }) {
|
||||||
|
@ -19,7 +19,7 @@ export function SettingsSectionTitle({ children }: { children: React.ReactNode }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSectionDescription({ children }: { children: React.ReactNode }) {
|
export function SettingsSectionDescription({ children }: { children: React.ReactNode }) {
|
||||||
return <p className="text-muted-foreground">{children}</p>
|
return <p className="text-muted-foreground text-sm">{children}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSectionBody({ children }: { children: React.ReactNode }) {
|
export function SettingsSectionBody({ children }: { children: React.ReactNode }) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function SettingsSectionTitle({
|
||||||
}: SettingsSectionTitleProps) {
|
}: SettingsSectionTitleProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`space-y-0.5 select-none ${!size || size === "2xl" ? "mb-8 md:mb-8" : ""}`}
|
className={`space-y-0.5 ${!size || size === "2xl" ? "mb-8 md:mb-8" : ""}`}
|
||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className={`text-${
|
className={`text-${
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function SidebarSettings({
|
||||||
<aside className="lg:w-1/5">
|
<aside className="lg:w-1/5">
|
||||||
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
||||||
</aside>
|
</aside>
|
||||||
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>
|
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""} space-y-6`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
53
src/components/StrategySelect.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||||
|
|
||||||
|
interface StrategyOption {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StrategySelectProps {
|
||||||
|
options: StrategyOption[];
|
||||||
|
defaultValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StrategySelect({
|
||||||
|
options,
|
||||||
|
defaultValue,
|
||||||
|
onChange
|
||||||
|
}: StrategySelectProps) {
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<label
|
||||||
|
key={option.id}
|
||||||
|
htmlFor={option.id}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer rounded-lg border-2 p-4",
|
||||||
|
"data-[state=checked]:border-primary data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={option.id}
|
||||||
|
id={option.id}
|
||||||
|
className="absolute left-4 top-5 h-4 w-4 border-primary text-primary"
|
||||||
|
/>
|
||||||
|
<div className="pl-7">
|
||||||
|
<div className="font-medium">{option.title}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{option.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}
|
|
@ -490,7 +490,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-row flex-wrap items-center gap-2 p-2 w-full rounded-md border-2 border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,
|
`flex flex-row flex-wrap items-center gap-1.5 p-1.5 w-full rounded-md border-2 border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 bg-transparent`,
|
||||||
styleClasses?.inlineTagsContainer
|
styleClasses?.inlineTagsContainer
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -644,7 +644,7 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex flex-row flex-wrap items-center p-2 gap-2 h-fit w-full bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,
|
`flex flex-row flex-wrap items-center p-1.5 gap-1.5 h-fit w-full bg-transparent text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,
|
||||||
styleClasses?.inlineTagsContainer
|
styleClasses?.inlineTagsContainer
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const tagVariants = cva(
|
||||||
"bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90 disabled:cursor-not-allowed disabled:opacity-50"
|
"bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: "text-xs h-7",
|
sm: "text-xs h-6",
|
||||||
md: "text-sm h-8",
|
md: "text-sm h-8",
|
||||||
lg: "text-base h-9",
|
lg: "text-base h-9",
|
||||||
xl: "text-lg h-10"
|
xl: "text-lg h-10"
|
||||||
|
@ -67,7 +67,7 @@ export const tagVariants = cva(
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "md",
|
size: "md",
|
||||||
shape: "default",
|
shape: "default",
|
||||||
borderStyle: "default",
|
borderStyle: "none",
|
||||||
interaction: "nonClickable",
|
interaction: "nonClickable",
|
||||||
animation: "fadeIn",
|
animation: "fadeIn",
|
||||||
textStyle: "normal"
|
textStyle: "normal"
|
||||||
|
@ -144,7 +144,7 @@ export const Tag: React.FC<TagProps> = ({
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
`py-1 px-3 h-full hover:bg-transparent`,
|
`p-1 h-full hover:bg-transparent`,
|
||||||
tagClasses?.closeButton
|
tagClasses?.closeButton
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -16,6 +16,8 @@ const buttonVariants = cva(
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
"border-2 border-input bg-card hover:bg-accent hover:text-accent-foreground",
|
"border-2 border-input bg-card hover:bg-accent hover:text-accent-foreground",
|
||||||
|
outlinePrimary:
|
||||||
|
"border-2 border-primary bg-card hover:bg-primary/10 text-primary",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary border border-input border-2 text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary border border-input border-2 text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
|
|
@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
"relative flex h-10 w-10 items-center justify-center border-y-2 border-r-2 border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l-2 last:rounded-r-md",
|
||||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -20,8 +20,8 @@ const SelectTrigger = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full items-center justify-between border-2 border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-9 w-full items-center justify-between border-2 border-input bg-card px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className,
|
"rounded-md",
|
||||||
"rounded-md"
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
|
@ -11,7 +11,7 @@ const Switch = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<SwitchPrimitives.Root
|
<SwitchPrimitives.Root
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer inline-flex h-4 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
|
||||||
>
|
>
|
||||||
<SwitchPrimitives.Thumb
|
<SwitchPrimitives.Thumb
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none block h-3 w-3 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SwitchPrimitives.Root>
|
</SwitchPrimitives.Root>
|
||||||
|
|
|
@ -77,7 +77,7 @@ const TableHead = React.forwardRef<
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
"h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
|
||||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
const toastVariants = cva(
|
const toastVariants = cva(
|
||||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|