Merge branch 'dev' of github.com:fosrl/pangolin into dev

This commit is contained in:
Owen
2025-12-06 21:37:24 -05:00
27 changed files with 3963 additions and 2061 deletions

View File

@@ -57,7 +57,8 @@ export default async function ClientsPage(props: ClientsPageProps) {
userId: client.userId,
username: client.username,
userEmail: client.userEmail,
niceId: client.niceId
niceId: client.niceId,
agent: client.agent
};
};

View File

@@ -53,7 +53,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
olmUpdateAvailable: client.olmUpdateAvailable || false,
userId: client.userId,
username: client.username,
userEmail: client.userEmail
userEmail: client.userEmail,
niceId: client.niceId,
agent: client.agent
};
};

View File

@@ -123,10 +123,9 @@ const addTargetSchema = z
ip: z.string().refine(isTargetValid),
method: z.string().nullable(),
port: z.coerce.number<number>().int().positive(),
siteId: z.int()
.positive({
error: "You must select a site for a target."
}),
siteId: z.int().positive({
error: "You must select a site for a target."
}),
path: z.string().optional().nullable(),
pathMatchType: z
.enum(["exact", "prefix", "regex"])
@@ -550,11 +549,11 @@ export default function ReverseProxyTargets(props: {
prev.map((t) =>
t.targetId === target.targetId
? {
...t,
targetId: response.data.data.targetId,
new: false,
updated: false
}
...t,
targetId: response.data.data.targetId,
new: false,
updated: false
}
: t
)
);
@@ -611,16 +610,16 @@ export default function ReverseProxyTargets(props: {
const newTarget: LocalTarget = {
...data,
path: isHttp ? (data.path || null) : null,
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
rewritePath: isHttp ? (data.rewritePath || null) : null,
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
path: isHttp ? data.path || null : null,
pathMatchType: isHttp ? data.pathMatchType || null : null,
rewritePath: isHttp ? data.rewritePath || null : null,
rewritePathType: isHttp ? data.rewritePathType || null : null,
siteType: site?.type || null,
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: resource.resourceId,
priority: isHttp ? (data.priority || 100) : 100,
priority: isHttp ? data.priority || 100 : 100,
hcEnabled: false,
hcPath: null,
hcMethod: null,
@@ -635,7 +634,7 @@ export default function ReverseProxyTargets(props: {
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null,
hcTlsServerName: null
};
setTargets([...targets, newTarget]);
@@ -657,11 +656,11 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
)
);
@@ -672,10 +671,10 @@ export default function ReverseProxyTargets(props: {
targets.map((target) =>
target.targetId === targetId
? {
...target,
...config,
updated: true
}
...target,
...config,
updated: true
}
: target
)
);
@@ -737,7 +736,7 @@ export default function ReverseProxyTargets(props: {
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName,
hcTlsServerName: target.hcTlsServerName
};
// Only include path-related fields for HTTP resources
@@ -837,7 +836,7 @@ export default function ReverseProxyTargets(props: {
const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority",
header: () => (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 p-3">
{t("priority")}
<TooltipProvider>
<Tooltip>
@@ -881,7 +880,7 @@ export default function ReverseProxyTargets(props: {
const healthCheckColumn: ColumnDef<LocalTarget> = {
accessorKey: "healthCheck",
header: () => (<span className="p-3">{t("healthCheck")}</span>),
header: () => <span className="p-3">{t("healthCheck")}</span>,
cell: ({ row }) => {
const status = row.original.hcHealth || "unknown";
const isEnabled = row.original.hcEnabled;
@@ -927,18 +926,16 @@ export default function ReverseProxyTargets(props: {
{row.original.siteType === "newt" ? (
<Button
variant="outline"
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
className="flex items-center gap-2 w-full text-left cursor-pointer"
onClick={() =>
openHealthCheckDialog(row.original)
}
>
<Badge variant={getStatusColor(status)}>
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Badge>
<Settings className="h-4 w-4" />
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Button>
) : (
<span>-</span>
@@ -953,7 +950,7 @@ export default function ReverseProxyTargets(props: {
const matchPathColumn: ColumnDef<LocalTarget> = {
accessorKey: "path",
header: () => (<span className="p-3">{t("matchPath")}</span>),
header: () => <span className="p-3">{t("matchPath")}</span>,
cell: ({ row }) => {
const hasPathMatch = !!(
row.original.path || row.original.pathMatchType
@@ -1015,7 +1012,7 @@ export default function ReverseProxyTargets(props: {
const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address",
header: () => (<span className="p-3">{t("address")}</span>),
header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
@@ -1068,7 +1065,7 @@ export default function ReverseProxyTargets(props: {
className={cn(
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
!row.original.siteId &&
"text-muted-foreground"
"text-muted-foreground"
)}
>
<span className="truncate max-w-[150px]">
@@ -1136,8 +1133,12 @@ export default function ReverseProxyTargets(props: {
{row.original.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
@@ -1151,7 +1152,7 @@ export default function ReverseProxyTargets(props: {
<Input
defaultValue={row.original.ip}
placeholder="IP / Hostname"
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();
@@ -1229,7 +1230,7 @@ export default function ReverseProxyTargets(props: {
const rewritePathColumn: ColumnDef<LocalTarget> = {
accessorKey: "rewritePath",
header: () => (<span className="p-3">{t("rewritePath")}</span>),
header: () => <span className="p-3">{t("rewritePath")}</span>,
cell: ({ row }) => {
const hasRewritePath = !!(
row.original.rewritePath || row.original.rewritePathType
@@ -1299,7 +1300,7 @@ export default function ReverseProxyTargets(props: {
const enabledColumn: ColumnDef<LocalTarget> = {
accessorKey: "enabled",
header: () => (<span className="p-3">{t("enabled")}</span>),
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<div className="flex items-center justify-center w-full">
<Switch
@@ -1320,7 +1321,7 @@ export default function ReverseProxyTargets(props: {
const actionsColumn: ColumnDef<LocalTarget> = {
id: "actions",
header: () => (<span className="p-3">{t("actions")}</span>),
header: () => <span className="p-3">{t("actions")}</span>,
cell: ({ row }) => (
<div className="flex items-center w-full">
<Button
@@ -1403,21 +1404,30 @@ export default function ReverseProxyTargets(props: {
<TableRow key={headerGroup.id}>
{headerGroup.headers.map(
(header) => {
const isActionsColumn = header.column.id === "actions";
const isActionsColumn =
header.column
.id ===
"actions";
return (
<TableHead
key={header.id}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
key={
header.id
}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
);
}
@@ -1434,13 +1444,20 @@ export default function ReverseProxyTargets(props: {
{row
.getVisibleCells()
.map((cell) => {
const isActionsColumn = cell.column.id === "actions";
const isActionsColumn =
cell.column
.id ===
"actions";
return (
<TableCell
key={
cell.id
}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{flexRender(
cell
@@ -1496,7 +1513,7 @@ export default function ReverseProxyTargets(props: {
</div>
</>
) : (
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
<div className="text-center p-4">
<p className="text-muted-foreground mb-4">
{t("targetNoOne")}
</p>
@@ -1725,7 +1742,9 @@ export default function ReverseProxyTargets(props: {
defaultChecked={
field.value || false
}
onCheckedChange={(val) => {
onCheckedChange={(
val
) => {
field.onChange(val);
}}
/>
@@ -1734,19 +1753,37 @@ export default function ReverseProxyTargets(props: {
)}
/>
{proxySettingsForm.watch("proxyProtocol") && (
{proxySettingsForm.watch(
"proxyProtocol"
) && (
<>
<FormField
control={proxySettingsForm.control}
control={
proxySettingsForm.control
}
name="proxyProtocolVersion"
render={({ field }) => (
<FormItem>
<FormLabel>{t("proxyProtocolVersion")}</FormLabel>
<FormLabel>
{t(
"proxyProtocolVersion"
)}
</FormLabel>
<FormControl>
<Select
value={String(field.value || 1)}
onValueChange={(value) =>
field.onChange(parseInt(value, 10))
value={String(
field.value ||
1
)}
onValueChange={(
value
) =>
field.onChange(
parseInt(
value,
10
)
)
}
>
<SelectTrigger>
@@ -1754,16 +1791,22 @@ export default function ReverseProxyTargets(props: {
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t("version1")}
{t(
"version1"
)}
</SelectItem>
<SelectItem value="2">
{t("version2")}
{t(
"version2"
)}
</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription>
{t("versionDescription")}
{t(
"versionDescription"
)}
</FormDescription>
</FormItem>
)}
@@ -1772,7 +1815,10 @@ export default function ReverseProxyTargets(props: {
<Alert>
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<strong>{t("warning")}:</strong> {t("proxyProtocolWarning")}
<strong>
{t("warning")}:
</strong>{" "}
{t("proxyProtocolWarning")}
</AlertDescription>
</Alert>
</>
@@ -1839,8 +1885,9 @@ export default function ReverseProxyTargets(props: {
hcUnhealthyInterval:
selectedTargetForHealthCheck.hcUnhealthyInterval ||
30,
hcTlsServerName: selectedTargetForHealthCheck.hcTlsServerName ||
undefined,
hcTlsServerName:
selectedTargetForHealthCheck.hcTlsServerName ||
undefined
}}
onChanges={async (config) => {
if (selectedTargetForHealthCheck) {

View File

@@ -436,16 +436,16 @@ export default function Page() {
const newTarget: LocalTarget = {
...data,
path: isHttp ? (data.path || null) : null,
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
rewritePath: isHttp ? (data.rewritePath || null) : null,
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
path: isHttp ? data.path || null : null,
pathMatchType: isHttp ? data.pathMatchType || null : null,
rewritePath: isHttp ? data.rewritePath || null : null,
rewritePathType: isHttp ? data.rewritePathType || null : null,
siteType: site?.type || null,
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: 0, // Will be set when resource is created
priority: isHttp ? (data.priority || 100) : 100, // Default priority
priority: isHttp ? data.priority || 100 : 100, // Default priority
hcEnabled: false,
hcPath: null,
hcMethod: null,
@@ -511,7 +511,7 @@ export default function Page() {
try {
const payload = {
name: baseData.name,
http: baseData.http,
http: baseData.http
};
let sanitizedSubdomain: string | undefined;
@@ -581,7 +581,8 @@ export default function Page() {
hcFollowRedirects:
target.hcFollowRedirects || null,
hcStatus: target.hcStatus || null,
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
hcUnhealthyInterval:
target.hcUnhealthyInterval || null,
hcMode: target.hcMode || null,
hcTlsServerName: target.hcTlsServerName
};
@@ -741,7 +742,7 @@ export default function Page() {
const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority",
header: () => (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 p-3">
{t("priority")}
<TooltipProvider>
<Tooltip>
@@ -784,7 +785,7 @@ export default function Page() {
const healthCheckColumn: ColumnDef<LocalTarget> = {
accessorKey: "healthCheck",
header: () => (<span className="p-3">{t("healthCheck")}</span>),
header: () => <span className="p-3">{t("healthCheck")}</span>,
cell: ({ row }) => {
const status = row.original.hcHealth || "unknown";
const isEnabled = row.original.hcEnabled;
@@ -830,18 +831,16 @@ export default function Page() {
{row.original.siteType === "newt" ? (
<Button
variant="outline"
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
className="flex items-center gap-2 w-full text-left cursor-pointer"
onClick={() =>
openHealthCheckDialog(row.original)
}
>
<Badge variant={getStatusColor(status)}>
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Badge>
<Settings className="h-4 w-4" />
<div className="flex items-center gap-1">
{getStatusIcon(status)}
{getStatusText(status)}
</div>
</Button>
) : (
<span>-</span>
@@ -856,7 +855,7 @@ export default function Page() {
const matchPathColumn: ColumnDef<LocalTarget> = {
accessorKey: "path",
header: () => (<span className="p-3">{t("matchPath")}</span>),
header: () => <span className="p-3">{t("matchPath")}</span>,
cell: ({ row }) => {
const hasPathMatch = !!(
row.original.path || row.original.pathMatchType
@@ -918,7 +917,7 @@ export default function Page() {
const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address",
header: () => (<span className="p-3">{t("address")}</span>),
header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
@@ -1039,8 +1038,12 @@ export default function Page() {
{row.original.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
@@ -1054,7 +1057,7 @@ export default function Page() {
<Input
defaultValue={row.original.ip}
placeholder="IP / Hostname"
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();
@@ -1132,7 +1135,7 @@ export default function Page() {
const rewritePathColumn: ColumnDef<LocalTarget> = {
accessorKey: "rewritePath",
header: () => (<span className="p-3">{t("rewritePath")}</span>),
header: () => <span className="p-3">{t("rewritePath")}</span>,
cell: ({ row }) => {
const hasRewritePath = !!(
row.original.rewritePath || row.original.rewritePathType
@@ -1202,7 +1205,7 @@ export default function Page() {
const enabledColumn: ColumnDef<LocalTarget> = {
accessorKey: "enabled",
header: () => (<span className="p-3">{t("enabled")}</span>),
header: () => <span className="p-3">{t("enabled")}</span>,
cell: ({ row }) => (
<div className="flex items-center justify-center w-full">
<Switch
@@ -1223,7 +1226,7 @@ export default function Page() {
const actionsColumn: ColumnDef<LocalTarget> = {
id: "actions",
header: () => (<span className="p-3">{t("actions")}</span>),
header: () => <span className="p-3">{t("actions")}</span>,
cell: ({ row }) => (
<div className="flex items-center justify-end w-full">
<Button
@@ -1345,42 +1348,38 @@ export default function Page() {
</form>
</Form>
</SettingsSectionForm>
{resourceTypes.length > 1 && (
<>
<div className="mb-2">
<span className="text-sm font-medium">
{t("type")}
</span>
</div>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
// Update method default when switching resource type
addTargetForm.setValue(
"method",
value === "http"
? "http"
: null
);
}}
cols={2}
/>
</>
)}
</SettingsSectionBody>
</SettingsSection>
{resourceTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceType")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
// Update method default when switching resource type
addTargetForm.setValue(
"method",
value === "http"
? "http"
: null
);
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{baseForm.watch("http") ? (
<SettingsSection>
<SettingsSectionHeader>
@@ -1426,146 +1425,98 @@ export default function Page() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tcpUdpForm}>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); // block default enter refresh
}
}}
className="space-y-4"
id="tcp-udp-settings-form"
>
<Controller
control={
tcpUdpForm.control
}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"protocol"
)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
{...field}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"protocolSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
tcpUdpForm.control
}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"resourcePortNumber"
)}
</FormLabel>
<Form {...tcpUdpForm}>
<form
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); // block default enter refresh
}
}}
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
id="tcp-udp-settings-form"
>
<Controller
control={tcpUdpForm.control}
name="protocol"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("protocol")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
{...field}
>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
{/* {build == "oss" && (
<FormField
control={
tcpUdpForm.control
}
name="enableProxy"
render={({
field
}) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
variant={
"outlinePrimarySquare"
}
checked={
field.value
}
onCheckedChange={
field.onChange
}
<SelectTrigger>
<SelectValue
placeholder={t(
"protocolSelect"
)}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{t(
"resourceEnableProxy"
)}
</FormLabel>
<FormDescription>
{t(
"resourceEnableProxyDescription"
)}
</FormDescription>
</div>
</FormItem>
)}
/>
)} */}
</form>
</Form>
</SettingsSectionForm>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tcp">
TCP
</SelectItem>
<SelectItem value="udp">
UDP
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={tcpUdpForm.control}
name="proxyPort"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"resourcePortNumber"
)}
</FormLabel>
<FormControl>
<Input
type="number"
value={
field.value ??
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
? parseInt(
e
.target
.value
)
: undefined
)
}
/>
</FormControl>
<FormMessage />
<FormDescription>
{t(
"resourcePortNumberDescription"
)}
</FormDescription>
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionBody>
</SettingsSection>
)}
@@ -1600,13 +1551,21 @@ export default function Page() {
(
header
) => {
const isActionsColumn = header.column.id === "actions";
const isActionsColumn =
header
.column
.id ===
"actions";
return (
<TableHead
key={
header.id
}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{header.isPlaceholder
? null
@@ -1643,13 +1602,21 @@ export default function Page() {
(
cell
) => {
const isActionsColumn = cell.column.id === "actions";
const isActionsColumn =
cell
.column
.id ===
"actions";
return (
<TableCell
key={
cell.id
}
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
className={
isActionsColumn
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: ""
}
>
{flexRender(
cell
@@ -1715,7 +1682,7 @@ export default function Page() {
</div>
</>
) : (
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
<div className="text-center p-4">
<p className="text-muted-foreground mb-4">
{t("targetNoOne")}
</p>

View File

@@ -809,15 +809,6 @@ export default function DomainPicker2({
)}
</div>
)}
{loadingDomains && (
<div className="flex items-center justify-center p-4">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary"></div>
<span>{t("domainPickerLoadingDomains")}</span>
</div>
</div>
)}
</div>
);
}

View File

@@ -154,7 +154,7 @@ export function LayoutSidebar({
</div>
</div>
<div className="p-4 flex flex-col shrink-0">
<div className="p-4 pt-1 flex flex-col shrink-0">
{canShowProductUpdates && (
<div className="mb-3">
<ProductUpdates isCollapsed={isSidebarCollapsed} />

View File

@@ -41,6 +41,7 @@ export type ClientRow = {
username: string | null;
userEmail: string | null;
niceId: string;
agent: string | null;
};
type ClientTableProps = {
@@ -65,7 +66,6 @@ export default function MachineClientsTable({
const [isRefreshing, startTransition] = useTransition();
const defaultMachineColumnVisibility = {
client: false,
subnet: false,
userId: false,
niceId: false
@@ -226,7 +226,7 @@ export default function MachineClientsTable({
},
{
accessorKey: "client",
friendlyName: t("client"),
friendlyName: t("agent"),
header: ({ column }) => {
return (
<Button
@@ -237,7 +237,7 @@ export default function MachineClientsTable({
)
}
>
{t("client")}
{t("agent")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -247,19 +247,18 @@ export default function MachineClientsTable({
return (
<div className="flex items-center space-x-1">
<Badge variant="secondary">
<div className="flex items-center space-x-2">
<span>Olm</span>
{originalRow.olmVersion && (
<span className="text-xs text-gray-500">
v{originalRow.olmVersion}
</span>
)}
</div>
</Badge>
{originalRow.olmUpdateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} />
{originalRow.agent && originalRow.olmVersion ? (
<Badge variant="secondary">
{originalRow.agent +
" v" +
originalRow.olmVersion}
</Badge>
) : (
"-"
)}
{/*originalRow.olmUpdateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} />
)*/}
</div>
);
}

View File

@@ -180,33 +180,33 @@ function ProductUpdatesListPopup({
<div
className={cn(
"relative z-1 cursor-pointer block",
"rounded-md border bg-muted p-2 py-3 w-full flex items-start gap-2 text-sm",
"rounded-md border bg-muted p-2 py-3 w-full flex flex-col gap-2 text-sm",
"transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full"
)}
>
<div className="rounded-md bg-muted-foreground/20 p-2">
<BellIcon className="flex-none size-4" />
</div>
<div className="flex flex-col gap-2 flex-1">
<div className="flex justify-between items-center w-full">
<div className="flex items-center gap-2">
<div className="rounded-md bg-muted-foreground/20 p-2">
<BellIcon className="flex-none size-4" />
</div>
<div className="flex justify-between items-center flex-1">
<p className="font-medium text-start">
{t("productUpdateWhatsNew")}
</p>
<div className="p-1 cursor-pointer ml-auto">
<div className="p-1 cursor-pointer">
<ChevronRightIcon className="size-4 flex-none" />
</div>
</div>
<small
className={cn(
"text-start text-muted-foreground",
"overflow-hidden h-8",
"[-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]"
)}
>
{updates[0]?.contents}
</small>
</div>
<small
className={cn(
"text-start text-muted-foreground",
"overflow-hidden h-8",
"[-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]"
)}
>
{updates[0]?.contents}
</small>
</div>
</PopoverTrigger>
</Transition>
@@ -332,20 +332,31 @@ function NewVersionAvailable({
<div
className={cn(
"relative z-2",
"rounded-md border bg-muted p-2 py-3 w-full flex items-start gap-2 text-sm",
"rounded-md border bg-muted p-2 py-3 w-full flex flex-col gap-2 text-sm",
"transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full"
)}
>
{version && (
<>
<div className="rounded-md bg-muted-foreground/20 p-2">
<RocketIcon className="flex-none size-4" />
</div>
<div className="flex flex-col gap-2">
<p className="font-medium">
<div className="flex items-center gap-2">
<div className="rounded-md bg-muted-foreground/20 p-2">
<RocketIcon className="flex-none size-4" />
</div>
<p className="font-medium flex-1">
{t("pangolinUpdateAvailable")}
</p>
<button
className="p-1 cursor-pointer"
onClick={() => {
setOpen(false);
onDimiss();
}}
>
<XIcon className="size-4 flex-none" />
</button>
</div>
<div className="flex flex-col gap-2">
<small className="text-muted-foreground">
{t("pangolinUpdateAvailableInfo", {
version: version.pangolin.latestVersion
@@ -362,15 +373,6 @@ function NewVersionAvailable({
<ArrowRight className="flex-none size-3" />
</a>
</div>
<button
className="p-1 cursor-pointer"
onClick={() => {
setOpen(false);
onDimiss();
}}
>
<XIcon className="size-4 flex-none" />
</button>
</>
)}
</div>

View File

@@ -40,6 +40,8 @@ export type ClientRow = {
userId: string | null;
username: string | null;
userEmail: string | null;
niceId: string;
agent: string | null;
};
type ClientTableProps = {
@@ -60,8 +62,8 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
const [isRefreshing, startTransition] = useTransition();
const defaultUserColumnVisibility = {
client: false,
subnet: false
subnet: false,
niceId: false
};
const refreshData = () => {
@@ -123,6 +125,25 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
);
}
},
{
accessorKey: "niceId",
friendlyName: t("identifier"),
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(
column.getIsSorted() === "asc"
)
}
>
{t("identifier")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: "userEmail",
friendlyName: "User",
@@ -261,7 +282,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
},
{
accessorKey: "client",
friendlyName: t("client"),
friendlyName: t("agent"),
header: ({ column }) => {
return (
<Button
@@ -272,7 +293,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
)
}
>
{t("client")}
{t("agent")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -282,19 +303,19 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
return (
<div className="flex items-center space-x-1">
<Badge variant="secondary">
<div className="flex items-center space-x-2">
<span>Olm</span>
{originalRow.olmVersion && (
<span className="text-xs text-gray-500">
v{originalRow.olmVersion}
</span>
)}
</div>
</Badge>
{originalRow.olmUpdateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} />
{originalRow.agent && originalRow.olmVersion ? (
<Badge variant="secondary">
{originalRow.agent +
" v" +
originalRow.olmVersion}
</Badge>
) : (
"-"
)}
{/*originalRow.olmUpdateAvailable && (
<InfoPopup info={t("olmUpdateAvailableInfo")} />
)*/}
</div>
);
}
@@ -320,62 +341,52 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
}
];
// Only include actions column if there are rows without userIds
if (hasRowsWithoutUserId) {
baseColumns.push({
id: "actions",
enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const clientRow = row.original;
return !clientRow.userId ? (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
Open menu
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* <Link */}
{/* className="block w-full" */}
{/* href={`/${clientRow.orgId}/settings/sites/${clientRow.nice}`} */}
{/* > */}
{/* <DropdownMenuItem> */}
{/* View settings */}
{/* </DropdownMenuItem> */}
{/* </Link> */}
<DropdownMenuItem
onClick={() => {
setSelectedClient(clientRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
Delete
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
>
<Button variant={"outline"}>
Edit
<ArrowRight className="ml-2 w-4 h-4" />
baseColumns.push({
id: "actions",
enableHiding: false,
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const clientRow = row.original;
return !clientRow.userId ? (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</Link>
</div>
) : null;
}
});
}
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* <Link */}
{/* className="block w-full" */}
{/* href={`/${clientRow.orgId}/settings/sites/${clientRow.nice}`} */}
{/* > */}
{/* <DropdownMenuItem> */}
{/* View settings */}
{/* </DropdownMenuItem> */}
{/* </Link> */}
<DropdownMenuItem
onClick={() => {
setSelectedClient(clientRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
>
<Button variant={"outline"}>
Edit
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</div>
) : null;
}
});
return baseColumns;
}, [hasRowsWithoutUserId, t]);