mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-17 04:47:11 +00:00
Merge branch 'dev' into feat/labels-on-sites-and-resources
This commit is contained in:
@@ -13,6 +13,8 @@ import DomainCertForm from "@app/components/DomainCertForm";
|
||||
import { build } from "@server/build";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Lock } from "lucide-react";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
|
||||
interface DomainPageClientProps {
|
||||
initialDomain: GetDomainResponse;
|
||||
@@ -49,7 +51,22 @@ export default function DomainPageClient({
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<SettingsSectionTitle
|
||||
title={domain.baseDomain}
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
{domain.baseDomain}
|
||||
{domain.configManaged && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 text-sm font-normal"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
{t("configManaged", {
|
||||
fallback: "Config Managed"
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
description={t("domainSettingDescription")}
|
||||
/>
|
||||
{env.flags.usePangolinDns && domain.failed ? (
|
||||
@@ -90,4 +107,4 @@ export default function DomainPageClient({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Lock } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CreateDomainForm from "@app/components/CreateDomainForm";
|
||||
import { useToast } from "@app/hooks/useToast";
|
||||
@@ -72,7 +73,11 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
const { org } = useOrgContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: rawDomains, isRefetching, refetch } = useQuery({
|
||||
const {
|
||||
data: rawDomains,
|
||||
isRefetching,
|
||||
refetch
|
||||
} = useQuery({
|
||||
...orgQueries.domains({ orgId }),
|
||||
initialData: domains as any,
|
||||
refetchInterval: durationToMs(10, "seconds")
|
||||
@@ -80,12 +85,15 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
(rawDomains ?? []).map((d) => ({
|
||||
...d,
|
||||
baseDomain: toUnicode(d.baseDomain),
|
||||
type: d.type ?? "",
|
||||
errorMessage: d.errorMessage ?? null
|
||||
} as DomainRow)),
|
||||
(rawDomains ?? []).map(
|
||||
(d) =>
|
||||
({
|
||||
...d,
|
||||
baseDomain: toUnicode(d.baseDomain),
|
||||
type: d.type ?? "",
|
||||
errorMessage: d.errorMessage ?? null
|
||||
}) as DomainRow
|
||||
),
|
||||
[rawDomains]
|
||||
);
|
||||
|
||||
@@ -198,12 +206,17 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="red" className="cursor-help">
|
||||
<Badge
|
||||
variant="red"
|
||||
className="cursor-help"
|
||||
>
|
||||
{t("failed", { fallback: "Failed" })}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="break-words">{errorMessage}</p>
|
||||
<p className="break-words">
|
||||
{errorMessage}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -220,12 +233,17 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="yellow" className="cursor-help">
|
||||
<Badge
|
||||
variant="yellow"
|
||||
className="cursor-help"
|
||||
>
|
||||
{t("pending")}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="break-words">{errorMessage}</p>
|
||||
<p className="break-words">
|
||||
{errorMessage}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -253,6 +271,25 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const domain = row.original;
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
{domain.baseDomain}
|
||||
{domain.configManaged && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1 text-xs font-normal"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
{t("configManaged", {
|
||||
fallback: "Config Managed"
|
||||
})}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
...(env.env.flags.usePangolinDns ? [typeColumn] : []),
|
||||
@@ -283,16 +320,18 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedDomain(domain);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{!domain.configManaged && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedDomain(domain);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{domain.failed && (
|
||||
@@ -315,7 +354,9 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
href={`/${orgId}/settings/domains/${domain.domainId}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
{domain.configManaged
|
||||
? t("view", { fallback: "View" })
|
||||
: t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -19,7 +19,8 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import { Textarea } from "@app/components/ui/textarea";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { Plus, X, AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
@@ -56,6 +57,8 @@ export interface Destination {
|
||||
sendActionLogs: boolean;
|
||||
sendConnectionLogs: boolean;
|
||||
sendRequestLogs: boolean;
|
||||
lastError: string | null;
|
||||
lastErrorAt: number | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
@@ -122,9 +125,7 @@ function HeadersEditor({ headers, onChange }: HeadersEditorProps) {
|
||||
/>
|
||||
<Input
|
||||
value={h.value}
|
||||
onChange={(e) =>
|
||||
updateRow(i, "value", e.target.value)
|
||||
}
|
||||
onChange={(e) => updateRow(i, "value", e.target.value)}
|
||||
placeholder={t("httpDestHeaderValuePlaceholder")}
|
||||
className="flex-1"
|
||||
/>
|
||||
@@ -200,10 +201,7 @@ export function HttpDestinationCredenza({
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
if (
|
||||
parsed.protocol !== "http:" &&
|
||||
parsed.protocol !== "https:"
|
||||
) {
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return t("httpDestUrlErrorHttpRequired");
|
||||
}
|
||||
if (build === "saas" && parsed.protocol !== "https:") {
|
||||
@@ -216,9 +214,7 @@ export function HttpDestinationCredenza({
|
||||
})();
|
||||
|
||||
const isValid =
|
||||
cfg.name.trim() !== "" &&
|
||||
cfg.url.trim() !== "" &&
|
||||
urlError === null;
|
||||
cfg.name.trim() !== "" && cfg.url.trim() !== "" && urlError === null;
|
||||
|
||||
async function handleSave() {
|
||||
if (!isValid) return;
|
||||
@@ -253,10 +249,7 @@ export function HttpDestinationCredenza({
|
||||
title: editing
|
||||
? t("httpDestUpdateFailed")
|
||||
: t("httpDestCreateFailed"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("streamingUnexpectedError")
|
||||
)
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -280,6 +273,14 @@ export function HttpDestinationCredenza({
|
||||
</CredenzaHeader>
|
||||
|
||||
<CredenzaBody>
|
||||
{editing?.lastError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="break-words">
|
||||
{editing.lastError}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<HorizontalTabs
|
||||
clientSide
|
||||
items={[
|
||||
@@ -357,7 +358,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestAuthNoneTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthNoneDescription")}
|
||||
{t(
|
||||
"httpDestAuthNoneDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,15 +378,21 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-bearer"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthBearerTitle")}
|
||||
{t(
|
||||
"httpDestAuthBearerTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthBearerDescription")}
|
||||
{t(
|
||||
"httpDestAuthBearerDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "bearer" && (
|
||||
<Input
|
||||
placeholder={t("httpDestAuthBearerPlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthBearerPlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.bearerToken ?? ""
|
||||
}
|
||||
@@ -411,15 +420,21 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-basic"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthBasicTitle")}
|
||||
{t(
|
||||
"httpDestAuthBasicTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthBasicDescription")}
|
||||
{t(
|
||||
"httpDestAuthBasicDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "basic" && (
|
||||
<Input
|
||||
placeholder={t("httpDestAuthBasicPlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthBasicPlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.basicCredentials ??
|
||||
""
|
||||
@@ -448,16 +463,22 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="auth-custom"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAuthCustomTitle")}
|
||||
{t(
|
||||
"httpDestAuthCustomTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAuthCustomDescription")}
|
||||
{t(
|
||||
"httpDestAuthCustomDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{cfg.authType === "custom" && (
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t("httpDestAuthCustomHeaderNamePlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthCustomHeaderNamePlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.customHeaderName ??
|
||||
""
|
||||
@@ -472,7 +493,9 @@ export function HttpDestinationCredenza({
|
||||
className="flex-1"
|
||||
/>
|
||||
<Input
|
||||
placeholder={t("httpDestAuthCustomHeaderValuePlaceholder")}
|
||||
placeholder={t(
|
||||
"httpDestAuthCustomHeaderValuePlaceholder"
|
||||
)}
|
||||
value={
|
||||
cfg.customHeaderValue ??
|
||||
""
|
||||
@@ -593,10 +616,14 @@ export function HttpDestinationCredenza({
|
||||
htmlFor="fmt-json-array"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestFormatJsonArrayTitle")}
|
||||
{t(
|
||||
"httpDestFormatJsonArrayTitle"
|
||||
)}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatJsonArrayDescription")}
|
||||
{t(
|
||||
"httpDestFormatJsonArrayDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,7 +643,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestFormatNdjsonTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatNdjsonDescription")}
|
||||
{t(
|
||||
"httpDestFormatNdjsonDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -636,7 +665,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestFormatSingleTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestFormatSingleDescription")}
|
||||
{t(
|
||||
"httpDestFormatSingleDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,7 +748,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestConnectionLogsTitle")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestConnectionLogsDescription")}
|
||||
{t(
|
||||
"httpDestConnectionLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -739,7 +772,9 @@ export function HttpDestinationCredenza({
|
||||
{t("httpDestRequestLogsTitle")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestRequestLogsDescription")}
|
||||
{t(
|
||||
"httpDestRequestLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -764,10 +799,12 @@ export function HttpDestinationCredenza({
|
||||
loading={saving}
|
||||
disabled={!isValid || saving}
|
||||
>
|
||||
{editing ? t("httpDestSaveChanges") : t("httpDestCreateDestination")}
|
||||
{editing
|
||||
? t("httpDestSaveChanges")
|
||||
: t("httpDestCreateDestination")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function InviteStatusCard({
|
||||
router.push(redirectUrl);
|
||||
} else if (!user && type === "not_logged_in") {
|
||||
const redirectUrl = email
|
||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||
router.push(redirectUrl);
|
||||
} else {
|
||||
@@ -113,7 +113,7 @@ export default function InviteStatusCard({
|
||||
async function goToLogin() {
|
||||
await api.post("/auth/logout", {});
|
||||
const redirectUrl = email
|
||||
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
|
||||
? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}`
|
||||
: `/auth/login?redirect=/invite?token=${tokenParam}`;
|
||||
router.push(redirectUrl);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import Link from "next/link";
|
||||
import { replacePlaceholder } from "@app/lib/replacePlaceholder";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type OrgLoginPageProps = {
|
||||
loginPage: LoadLoginPageResponse | undefined;
|
||||
@@ -52,19 +53,21 @@ export default async function OrgLoginPage({
|
||||
const t = await getTranslations();
|
||||
return (
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{env.branding.appName || "Pangolin"}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
{build !== "enterprise" || !env.branding.hidePoweredBy ? (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
{env.branding.appName || "Pangolin"}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
{branding?.logoUrl && (
|
||||
|
||||
@@ -375,7 +375,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
{!accessDenied ? (
|
||||
<div>
|
||||
{isUnlocked() && build === "enterprise" ? (
|
||||
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
||||
!env.branding.resourceAuthPage?.hidePoweredBy &&
|
||||
!env.branding.hidePoweredBy && (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
@@ -12,13 +12,64 @@ import {
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ContactSalesBanner } from "@app/components/ContactSalesBanner";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Destination } from "@app/components/HttpDestinationCredenza";
|
||||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export type S3PayloadFormat = "json_array" | "ndjson" | "csv";
|
||||
|
||||
export interface S3Config {
|
||||
name: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
prefix: string;
|
||||
endpoint: string;
|
||||
format: S3PayloadFormat;
|
||||
gzip: boolean;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const defaultS3Config = (): S3Config => ({
|
||||
name: "",
|
||||
accessKeyId: "",
|
||||
secretAccessKey: "",
|
||||
region: "us-east-1",
|
||||
bucket: "",
|
||||
prefix: "",
|
||||
endpoint: "",
|
||||
format: "json_array",
|
||||
gzip: false
|
||||
});
|
||||
|
||||
export function parseS3Config(raw: string): S3Config {
|
||||
try {
|
||||
return { ...defaultS3Config(), ...JSON.parse(raw) };
|
||||
} catch {
|
||||
return defaultS3Config();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Component ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface S3DestinationCredenzaProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editing: any;
|
||||
editing: Destination | null;
|
||||
orgId: string;
|
||||
onSaved: () => void;
|
||||
}
|
||||
@@ -28,18 +79,84 @@ export function S3DestinationCredenza({
|
||||
onOpenChange,
|
||||
editing,
|
||||
orgId,
|
||||
onSaved,
|
||||
onSaved
|
||||
}: S3DestinationCredenzaProps) {
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [cfg, setCfg] = useState<S3Config>(defaultS3Config());
|
||||
const [sendAccessLogs, setSendAccessLogs] = useState(false);
|
||||
const [sendActionLogs, setSendActionLogs] = useState(false);
|
||||
const [sendConnectionLogs, setSendConnectionLogs] = useState(false);
|
||||
const [sendRequestLogs, setSendRequestLogs] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCfg(editing ? parseS3Config(editing.config) : defaultS3Config());
|
||||
setSendAccessLogs(editing?.sendAccessLogs ?? false);
|
||||
setSendActionLogs(editing?.sendActionLogs ?? false);
|
||||
setSendConnectionLogs(editing?.sendConnectionLogs ?? false);
|
||||
setSendRequestLogs(editing?.sendRequestLogs ?? false);
|
||||
}
|
||||
}, [open, editing]);
|
||||
|
||||
const update = (patch: Partial<S3Config>) =>
|
||||
setCfg((prev) => ({ ...prev, ...patch }));
|
||||
|
||||
const isValid =
|
||||
cfg.name.trim() !== "" &&
|
||||
cfg.accessKeyId.trim() !== "" &&
|
||||
cfg.secretAccessKey.trim() !== "" &&
|
||||
cfg.region.trim() !== "" &&
|
||||
cfg.bucket.trim() !== "";
|
||||
|
||||
async function handleSave() {
|
||||
if (!isValid) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
type: "s3",
|
||||
config: JSON.stringify(cfg),
|
||||
sendAccessLogs,
|
||||
sendActionLogs,
|
||||
sendConnectionLogs,
|
||||
sendRequestLogs
|
||||
};
|
||||
if (editing) {
|
||||
await api.post(
|
||||
`/org/${orgId}/event-streaming-destination/${editing.destinationId}`,
|
||||
payload
|
||||
);
|
||||
toast({ title: t("s3DestUpdatedSuccess") });
|
||||
} else {
|
||||
await api.put(
|
||||
`/org/${orgId}/event-streaming-destination`,
|
||||
payload
|
||||
);
|
||||
toast({ title: t("s3DestCreatedSuccess") });
|
||||
}
|
||||
onSaved();
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: editing
|
||||
? t("s3DestUpdateFailed")
|
||||
: t("s3DestCreateFailed"),
|
||||
description: formatAxiosError(e, t("streamingUnexpectedError"))
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={onOpenChange}>
|
||||
<CredenzaContent className="sm:max-w-2xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{editing
|
||||
? t("S3DestEditTitle")
|
||||
: t("S3DestAddTitle")}
|
||||
{editing ? t("S3DestEditTitle") : t("S3DestAddTitle")}
|
||||
</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{editing
|
||||
@@ -49,13 +166,375 @@ export function S3DestinationCredenza({
|
||||
</CredenzaHeader>
|
||||
|
||||
<CredenzaBody>
|
||||
<ContactSalesBanner />
|
||||
{editing?.lastError && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription className="break-words">
|
||||
{editing.lastError}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<HorizontalTabs
|
||||
clientSide
|
||||
items={[
|
||||
{ title: t("s3DestTabSettings"), href: "" },
|
||||
{ title: t("s3DestTabFormat"), href: "" },
|
||||
{ title: t("httpDestTabLogs"), href: "" }
|
||||
]}
|
||||
>
|
||||
{/* ── Settings tab ────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
{/* Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-name">
|
||||
{t("s3DestNameLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-name"
|
||||
placeholder={t("s3DestNamePlaceholder")}
|
||||
value={cfg.name}
|
||||
onChange={(e) =>
|
||||
update({ name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AWS Access Key ID */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-access-key-id">
|
||||
{t("s3DestAccessKeyIdLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-access-key-id"
|
||||
placeholder="AKIAIOSFODNN7EXAMPLE"
|
||||
value={cfg.accessKeyId}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
accessKeyId: e.target.value
|
||||
})
|
||||
}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AWS Secret Access Key */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-secret-key">
|
||||
{t("s3DestSecretAccessKeyLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-secret-key"
|
||||
type="password"
|
||||
placeholder={t(
|
||||
"s3DestSecretAccessKeyPlaceholder"
|
||||
)}
|
||||
value={cfg.secretAccessKey}
|
||||
onChange={(e) =>
|
||||
update({
|
||||
secretAccessKey: e.target.value
|
||||
})
|
||||
}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Region */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-region">
|
||||
{t("s3DestRegionLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-region"
|
||||
placeholder="us-east-1"
|
||||
value={cfg.region}
|
||||
onChange={(e) =>
|
||||
update({ region: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bucket */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-bucket">
|
||||
{t("s3DestBucketLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-bucket"
|
||||
placeholder="my-logs-bucket"
|
||||
value={cfg.bucket}
|
||||
onChange={(e) =>
|
||||
update({ bucket: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Prefix */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-prefix">
|
||||
{t("s3DestPrefixLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-prefix"
|
||||
placeholder="pangolin/logs"
|
||||
value={cfg.prefix}
|
||||
onChange={(e) =>
|
||||
update({ prefix: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("s3DestPrefixDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom endpoint (optional – for S3-compatible storage) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="s3-endpoint">
|
||||
{t("s3DestEndpointLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="s3-endpoint"
|
||||
placeholder="https://s3.example.com"
|
||||
value={cfg.endpoint}
|
||||
onChange={(e) =>
|
||||
update({ endpoint: e.target.value })
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("s3DestEndpointDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Format tab ───────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
{/* Gzip compression toggle */}
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Switch
|
||||
id="s3-gzip"
|
||||
checked={cfg.gzip}
|
||||
onCheckedChange={(v) => update({ gzip: v })}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-gzip"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("s3DestGzipLabel")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("s3DestGzipDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payload format selector */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="font-medium block">
|
||||
{t("s3DestFormatTitle")}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{t("s3DestFormatDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
value={cfg.format}
|
||||
onValueChange={(v) =>
|
||||
update({
|
||||
format: v as S3PayloadFormat
|
||||
})
|
||||
}
|
||||
className="gap-2"
|
||||
>
|
||||
{/* JSON Array */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="json_array"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t(
|
||||
"httpDestFormatJsonArrayTitle"
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatJsonArrayDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* NDJSON */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="ndjson"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("httpDestFormatNdjsonTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatNdjsonDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* CSV */}
|
||||
<label className="flex items-start gap-3 rounded-md border p-3 cursor-pointer has-[:checked]:border-primary has-[:checked]:bg-primary/5">
|
||||
<RadioGroupItem
|
||||
value="csv"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("s3DestFormatCsvTitle")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{t(
|
||||
"s3DestFormatCsvDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Logs tab ──────────────────────────────────── */}
|
||||
<div className="space-y-6 mt-4 p-1">
|
||||
<div>
|
||||
<label className="font-medium block">
|
||||
{t("httpDestLogTypesTitle")}
|
||||
</label>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{t("httpDestLogTypesDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-access"
|
||||
checked={sendAccessLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendAccessLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-access"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestAccessLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestAccessLogsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-action"
|
||||
checked={sendActionLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendActionLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-action"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestActionLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t("httpDestActionLogsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-connection"
|
||||
checked={sendConnectionLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendConnectionLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-connection"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestConnectionLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t(
|
||||
"httpDestConnectionLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 rounded-md border p-3">
|
||||
<Checkbox
|
||||
id="s3-log-request"
|
||||
checked={sendRequestLogs}
|
||||
onCheckedChange={(v) =>
|
||||
setSendRequestLogs(v === true)
|
||||
}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="s3-log-request"
|
||||
className="cursor-pointer font-medium"
|
||||
>
|
||||
{t("httpDestRequestLogsTitle")}
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{t(
|
||||
"httpDestRequestLogsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HorizontalTabs>
|
||||
</CredenzaBody>
|
||||
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("cancel")}</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={saving}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</CredenzaClose>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!isValid || saving}
|
||||
>
|
||||
{editing
|
||||
? t("s3DestSaveChanges")
|
||||
: t("s3DestCreateDestination")}
|
||||
</Button>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
|
||||
@@ -61,6 +61,8 @@ export default function ShareLinksTable({
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedLink, setSelectedLink] = useState<ShareLinkRow | null>(null);
|
||||
const [rows, setRows] = useState<ShareLinkRow[]>(shareLinks);
|
||||
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -92,6 +94,7 @@ export default function ShareLinksTable({
|
||||
title: t("shareErrorDelete"),
|
||||
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
|
||||
});
|
||||
throw e;
|
||||
});
|
||||
|
||||
const newRows = rows.filter((r) => r.accessTokenId !== id);
|
||||
@@ -293,9 +296,10 @@ export default function ShareLinksTable({
|
||||
{/* </DropdownMenu> */}
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() =>
|
||||
deleteSharelink(row.original.accessTokenId)
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedLink(resourceRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
@@ -307,6 +311,30 @@ export default function ShareLinksTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedLink && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
if (!val) setSelectedLink(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("shareQuestionRemove")}</p>
|
||||
<p>{t("shareMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("shareDeleteConfirm")}
|
||||
onConfirm={async () =>
|
||||
deleteSharelink(selectedLink.accessTokenId)
|
||||
}
|
||||
string={
|
||||
selectedLink.title || selectedLink.resourceName
|
||||
}
|
||||
title={t("shareDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateShareLinkForm
|
||||
open={isCreateModalOpen}
|
||||
setOpen={setIsCreateModalOpen}
|
||||
|
||||
@@ -99,6 +99,14 @@ export default function UsersTable({
|
||||
];
|
||||
}, [searchParams.toString()]);
|
||||
|
||||
const isRemovingSelf = useMemo(() => {
|
||||
if (!selectedUser || !user) return false;
|
||||
return (
|
||||
`${selectedUser.username}-${selectedUser.idpId}` ===
|
||||
`${user.username}-${user.idpId}`
|
||||
);
|
||||
}, [selectedUser, user]);
|
||||
|
||||
function handleFilterChange(
|
||||
column: string,
|
||||
value: string | undefined | null
|
||||
@@ -223,10 +231,7 @@ export default function UsersTable({
|
||||
header: () => <span className="p-3"></span>,
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
const isCurrentUser =
|
||||
`${userRow.username}-${userRow.idpId}` ===
|
||||
`${user?.username}-${user?.idpId}`;
|
||||
const isDisabled = userRow.isOwner || isCurrentUser;
|
||||
const canRemoveFromOrg = !userRow.isOwner;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div>
|
||||
@@ -235,7 +240,6 @@ export default function UsersTable({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
@@ -247,16 +251,12 @@ export default function UsersTable({
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
className="block w-full"
|
||||
aria-disabled={isDisabled}
|
||||
onClick={(e) =>
|
||||
isDisabled && e.preventDefault()
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem disabled={isDisabled}>
|
||||
<DropdownMenuItem>
|
||||
{t("accessUserManage")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{!isDisabled && (
|
||||
{canRemoveFromOrg && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
@@ -271,25 +271,14 @@ export default function UsersTable({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{isDisabled ? (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled
|
||||
>
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"} className="ml-2">
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -359,22 +348,45 @@ export default function UsersTable({
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
<p>{t("userQuestionOrgRemove")}</p>
|
||||
<p>{t("userMessageOrgRemove")}</p>
|
||||
<p>
|
||||
{t(
|
||||
isRemovingSelf
|
||||
? "userQuestionOrgRemoveSelf"
|
||||
: "userQuestionOrgRemove"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
isRemovingSelf
|
||||
? "userMessageOrgRemoveSelf"
|
||||
: "userMessageOrgRemove"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("userRemoveOrgConfirm")}
|
||||
buttonText={t(
|
||||
isRemovingSelf
|
||||
? "userRemoveOrgConfirmSelf"
|
||||
: "userRemoveOrgConfirm"
|
||||
)}
|
||||
warningText={
|
||||
isRemovingSelf ? t("userRemoveOrgSelfWarning") : undefined
|
||||
}
|
||||
onConfirm={async () => startTransition(removeUser)}
|
||||
string={
|
||||
selectedUser
|
||||
? getUserDisplayName({
|
||||
email: selectedUser.email,
|
||||
name: selectedUser.name,
|
||||
username: selectedUser.username
|
||||
})
|
||||
: ""
|
||||
isRemovingSelf
|
||||
? t("userRemoveOrgConfirmPhraseSelf")
|
||||
: selectedUser
|
||||
? getUserDisplayName({
|
||||
email: selectedUser.email,
|
||||
name: selectedUser.name,
|
||||
username: selectedUser.username
|
||||
})
|
||||
: ""
|
||||
}
|
||||
title={t("userRemoveOrg")}
|
||||
title={t(
|
||||
isRemovingSelf ? "userRemoveOrgSelf" : "userRemoveOrg"
|
||||
)}
|
||||
/>
|
||||
|
||||
<ControlledDataTable
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from "@app/lib/cn";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export type TagValue = { text: string; id: string };
|
||||
export type TagValue = { text: string; id: string; isAdmin?: boolean };
|
||||
|
||||
export type MultiSelectTagsProps<T extends TagValue> = {
|
||||
emptyPlaceholder?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDebounce } from "use-debounce";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
|
||||
|
||||
export type SelectedRole = { id: string; text: string };
|
||||
export type SelectedRole = { id: string; text: string; isAdmin?: boolean };
|
||||
|
||||
export type RolesSelectorProps = {
|
||||
orgId: string;
|
||||
|
||||
Reference in New Issue
Block a user