Merge pull request #3331 from Fredkiss3/feat/geoip-tag-in-tables

feat: Show GeoIp country flags in site & rules page
This commit is contained in:
Owen Schwartz
2026-06-25 11:23:42 -07:00
committed by GitHub
4 changed files with 56 additions and 18 deletions

View File

@@ -795,10 +795,13 @@ export const COUNTRIES = [
name: "Serbia", name: "Serbia",
code: "RS" code: "RS"
}, },
{ // Removed as this is a deprecated ISO country code, not supported anymore
name: "Serbia and Montenegro", // Also the individual flags for Serbia & Montenegro are already included in the list
code: "CS" // more details: https://en.wikipedia.org/wiki/ISO_3166-2:CS
}, // {
// name: "Serbia and Montenegro",
// code: "CS"
// },
{ {
name: "Seychelles", name: "Seychelles",
code: "SC" code: "SC"

View File

@@ -10,6 +10,7 @@ import logger from "@server/logger";
import stoi from "@server/lib/stoi"; import stoi from "@server/lib/stoi";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { getCountryCodeForIp } from "@server/lib/geoip";
const getSiteSchema = z.strictObject({ const getSiteSchema = z.strictObject({
siteId: z siteId: z
@@ -47,6 +48,7 @@ type SiteQueryRow = NonNullable<Awaited<ReturnType<typeof query>>>;
export type GetSiteResponse = SiteQueryRow["sites"] & { export type GetSiteResponse = SiteQueryRow["sites"] & {
newtId: string | null; newtId: string | null;
newtVersion: string | null; newtVersion: string | null;
countryCode: string | null;
}; };
registry.registerPath({ registry.registerPath({
@@ -134,7 +136,10 @@ export async function getSite(
const data: GetSiteResponse = { const data: GetSiteResponse = {
...site.sites, ...site.sites,
newtId: site.newt ? site.newt.newtId : null, newtId: site.newt ? site.newt.newtId : null,
newtVersion: site.newt?.version ?? null newtVersion: site.newt?.version ?? null,
countryCode: site.sites.endpoint
? ((await getCountryCodeForIp(site.sites.endpoint)) ?? null)
: null
}; };
return response<GetSiteResponse>(res, { return response<GetSiteResponse>(res, {

View File

@@ -9,6 +9,7 @@ import {
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
type SiteInfoCardProps = {}; type SiteInfoCardProps = {};
@@ -52,7 +53,11 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("publicIpEndpoint")}</InfoSectionTitle> <InfoSectionTitle>{t("publicIpEndpoint")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{formatPublicEndpoint(site.endpoint)} {formatPublicEndpoint(site.endpoint)}&nbsp;
<span className="text-lg">
{site.countryCode &&
countryCodeToFlagEmoji(site.countryCode)}
</span>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
) : null; ) : null;

View File

@@ -74,6 +74,7 @@ import {
sortPolicyRulesForResourceOverlay, sortPolicyRulesForResourceOverlay,
type PolicyAccessRule type PolicyAccessRule
} from "./policy-access-rule-utils"; } from "./policy-access-rule-utils";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
export type PolicyAccessRulesTableProps = { export type PolicyAccessRulesTableProps = {
rules: PolicyAccessRule[]; rules: PolicyAccessRule[];
@@ -490,8 +491,17 @@ export function PolicyAccessRulesTable({
{ {
accessorKey: "value", accessorKey: "value",
header: () => <span className="p-3">{t("value")}</span>, header: () => <span className="p-3">{t("value")}</span>,
cell: ({ row }) => cell: ({ row }) => {
row.original.match === "COUNTRY" ? ( let selectedCountry: (typeof COUNTRIES)[number] | undefined;
if (
row.original.match === "COUNTRY" &&
row.original.value
) {
selectedCountry = COUNTRIES.find(
(c) => c.code === row.original.value
);
}
return row.original.match === "COUNTRY" ? (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@@ -502,15 +512,22 @@ export function PolicyAccessRulesTable({
} }
className="w-full min-w-0 justify-between" className="w-full min-w-0 justify-between"
> >
{row.original.value {selectedCountry ? (
? COUNTRIES.find( <>
(c) => <span>
c.code === row.original.value {selectedCountry.code === "ALL"
)?.name + ? "🌍"
" (" + : countryCodeToFlagEmoji(
row.original.value + selectedCountry.code
")" )}
: t("selectCountry")} &nbsp;&nbsp;
{selectedCountry.name} (
{selectedCountry.code})
</span>
</>
) : (
t("selectCountry")
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -540,6 +557,13 @@ export function PolicyAccessRulesTable({
<Check <Check
className={`mr-2 h-4 w-4 ${row.original.value === country.code ? "opacity-100" : "opacity-0"}`} className={`mr-2 h-4 w-4 ${row.original.value === country.code ? "opacity-100" : "opacity-0"}`}
/> />
<span>
{country.code === "ALL"
? "🌍"
: countryCodeToFlagEmoji(
country.code
)}
</span>
{country.name} ( {country.name} (
{country.code}) {country.code})
</CommandItem> </CommandItem>
@@ -767,7 +791,8 @@ export function PolicyAccessRulesTable({
}); });
}} }}
/> />
) );
}
}, },
{ {
accessorKey: "enabled", accessorKey: "enabled",