mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-13 16:36:41 +00:00
Merge branch 'dev' of github.com:fosrl/pangolin into dev
This commit is contained in:
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user