mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 01:46:38 +00:00
Merge branch 'dev' into refactor/domain-picker-default-value
This commit is contained in:
@@ -304,7 +304,7 @@ export default function ExitNodesTable({
|
||||
setSelectedNode(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("remoteExitNodeQuestionRemove")}</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||
|
||||
@@ -289,7 +289,7 @@ export default function GeneralPage() {
|
||||
setIsDeleteModalOpen(val);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("orgQuestionRemove")}</p>
|
||||
<p>{t("orgMessageRemove")}</p>
|
||||
</div>
|
||||
@@ -303,7 +303,7 @@ export default function GeneralPage() {
|
||||
open={isSecurityPolicyConfirmOpen}
|
||||
setOpen={setIsSecurityPolicyConfirmOpen}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("securityPolicyChangeDescription")}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useTransition } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||
@@ -21,21 +17,22 @@ import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusCo
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import axios from "axios";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
resources: {
|
||||
@@ -70,9 +67,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("access-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
@@ -91,11 +86,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -148,7 +143,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "access-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -309,8 +303,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -339,11 +331,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -631,7 +633,7 @@ export default function GeneralPage() {
|
||||
title={t("accessLogs")}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, User } from "lucide-react";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { build } from "@server/build";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { Key, User } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
@@ -34,7 +30,7 @@ export default function GeneralPage() {
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
actions: string[];
|
||||
@@ -58,9 +54,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("action-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
@@ -79,11 +73,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -136,7 +130,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "action-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -293,8 +286,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -323,11 +314,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -484,7 +485,7 @@ export default function GeneralPage() {
|
||||
searchColumn="action"
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
|
||||
// Pagination state
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
@@ -36,9 +34,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("request-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
|
||||
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
@@ -95,11 +91,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -152,7 +148,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "request-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -302,8 +297,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -335,11 +328,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -773,7 +776,7 @@ export default function GeneralPage() {
|
||||
searchColumn="host"
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -67,7 +67,10 @@ export default async function ClientResourcesPage(
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
alias: siteResource.alias || null,
|
||||
siteNiceId: siteResource.siteNiceId,
|
||||
niceId: siteResource.niceId
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||
disableIcmp: siteResource.disableIcmp || false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -225,7 +225,7 @@ export default function GeneralForm() {
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: resource.fullDomain,
|
||||
fullDomain: updated.fullDomain,
|
||||
proxyPort: data.proxyPort
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
|
||||
@@ -449,15 +449,16 @@ export default function ResourceRules(props: {
|
||||
type="number"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z
|
||||
const parsed = z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.safeParse(e.target.value);
|
||||
|
||||
if (!parsed.data) {
|
||||
if (!parsed.success) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("rulesErrorInvalidIpAddress"), // correct priority or IP?
|
||||
title: t("rulesErrorInvalidPriority"), // correct priority or IP?
|
||||
description: t(
|
||||
"rulesErrorInvalidPriorityDescription"
|
||||
)
|
||||
|
||||
@@ -315,7 +315,7 @@ export default function LicensePage() {
|
||||
setSelectedLicenseKey(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("licenseQuestionRemove")}</p>
|
||||
<p>
|
||||
<b>{t("licenseMessageRemove")}</b>
|
||||
@@ -360,7 +360,8 @@ export default function LicensePage() {
|
||||
<div className="space-y-2 text-green-500">
|
||||
<div className="text-2xl flex items-center gap-2">
|
||||
<Check />
|
||||
{t("licensed")}
|
||||
{t("licensed") +
|
||||
`${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -243,7 +243,7 @@ export default function UsersTable({ users }: Props) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("userQuestionRemove")}</p>
|
||||
|
||||
<p>{t("userMessageRemove")}</p>
|
||||
|
||||
@@ -23,6 +23,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||
const t = await getTranslations();
|
||||
let hideFooter = false;
|
||||
|
||||
let licenseStatus: GetLicenseStatusResponse | null = null;
|
||||
if (build == "enterprise") {
|
||||
const licenseStatusRes = await cache(
|
||||
async () =>
|
||||
@@ -30,10 +31,12 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||
"/license/status"
|
||||
)
|
||||
)();
|
||||
licenseStatus = licenseStatusRes.data.data;
|
||||
if (
|
||||
env.branding.hideAuthLayoutFooter &&
|
||||
licenseStatusRes.data.data.isHostLicensed &&
|
||||
licenseStatusRes.data.data.isLicenseValid
|
||||
licenseStatusRes.data.data.isLicenseValid &&
|
||||
licenseStatusRes.data.data.tier !== "personal"
|
||||
) {
|
||||
hideFooter = true;
|
||||
}
|
||||
@@ -83,6 +86,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||
? t("enterpriseEdition")
|
||||
: t("pangolinCloud")}
|
||||
</span>
|
||||
{build === "enterprise" &&
|
||||
licenseStatus?.isHostLicensed &&
|
||||
licenseStatus?.isLicenseValid &&
|
||||
licenseStatus?.tier === "personal" ? (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<span>{t("personalUseOnly")}</span>
|
||||
</>
|
||||
) : null}
|
||||
{build === "enterprise" &&
|
||||
(!licenseStatus?.isHostLicensed ||
|
||||
!licenseStatus?.isLicenseValid) ? (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<span>{t("unlicensed")}</span>
|
||||
</>
|
||||
) : null}
|
||||
{build === "saas" && (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
|
||||
Reference in New Issue
Block a user