Replug custom domain setting

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-29 11:23:59 +01:00
committed by Bryan Frimin
parent 278411392d
commit 6a97be5d8e
18 changed files with 1085 additions and 566 deletions

View File

@@ -1,155 +0,0 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, Badge, IconPlusLarge } from "@probo/ui";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { graphql } from "relay-runtime";
import type { CustomDomainManagerDeleteMutation } from "./__generated__/CustomDomainManagerDeleteMutation.graphql";
import { CreateCustomDomainDialog } from "./CreateCustomDomainDialog";
import { DeleteCustomDomainDialog } from "./DeleteCustomDomainDialog";
import { DomainDetailsDialog } from "./DomainDetailsDialog";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const deleteCustomDomainMutation = graphql`
mutation CustomDomainManagerDeleteMutation($input: DeleteCustomDomainInput!) {
deleteCustomDomain(input: $input) {
deletedCustomDomainId
}
}
`;
type CustomDomain = {
readonly id: string;
readonly domain: string;
readonly sslStatus: string;
readonly dnsRecords?:
| readonly {
readonly type: string;
readonly name: string;
readonly value: string;
readonly ttl?: number;
readonly purpose: string;
}[]
| null;
readonly createdAt?: string | null;
readonly updatedAt?: string | null;
readonly sslExpiresAt?: string | null;
};
interface CustomDomainManagerProps {
organizationId: string;
customDomain: CustomDomain | null | undefined;
}
export function CustomDomainManager({
organizationId,
customDomain,
}: CustomDomainManagerProps) {
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const [deleteCustomDomain] =
useMutationWithToasts<CustomDomainManagerDeleteMutation>(
deleteCustomDomainMutation,
{
successMessage: __("Domain deleted successfully"),
errorMessage: __("Failed to delete domain"),
}
);
const domain = customDomain;
const getStatusBadge = (domain: CustomDomain) => {
if (domain.sslStatus === "ACTIVE") {
return <Badge variant="success">{__("Active")}</Badge>;
}
if (
domain.sslStatus === "PROVISIONING" ||
domain.sslStatus === "RENEWING"
) {
return <Badge variant="warning">{__("Provisioning")}</Badge>;
}
if (domain.sslStatus === "PENDING") {
return <Badge variant="warning">{__("Pending")}</Badge>;
}
if (domain.sslStatus === "FAILED") {
return <Badge variant="danger">{__("Failed")}</Badge>;
}
if (domain.sslStatus === "EXPIRED") {
return <Badge variant="danger">{__("Expired")}</Badge>;
}
return <Badge variant="neutral">{__("Unknown")}</Badge>;
};
const handleDeleteDomain = async () => {
return deleteCustomDomain({
variables: {
input: { organizationId },
},
updater: (store) => {
// Update the cache by setting customDomain to null
const organizationRecord = store.get(organizationId);
if (organizationRecord) {
organizationRecord.setValue(null, "customDomain");
}
},
});
};
if (!domain) {
return (
<Card padded>
<div className="text-center py-8">
<h3 className="text-lg font-semibold mb-2">
{__("No custom domain configured")}
</h3>
<p className="text-txt-tertiary mb-4">
{__(
"Add your own domain to make your trust center more professional"
)}
</p>
<div className="flex justify-center">
{isAuthorized("Organization", "createCustomDomain") && (
<CreateCustomDomainDialog organizationId={organizationId}>
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
</CreateCustomDomainDialog>
)}
</div>
</div>
</Card>
);
}
return (
<Card>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div>
<div className="font-medium mb-1">{domain.domain}</div>
<div className="text-sm text-txt-secondary">
{domain.sslStatus === "ACTIVE"
? __("Verified")
: __("Pending verification")}
</div>
</div>
{getStatusBadge(domain)}
</div>
<div className="flex items-center gap-2">
<DomainDetailsDialog domain={domain}>
<Button variant="secondary">{__("View Details")}</Button>
</DomainDetailsDialog>
{isAuthorized("CustomDomain", "deleteCustomDomain") && (
<DeleteCustomDomainDialog
domainName={domain.domain}
onConfirm={handleDeleteDomain}
>
<Button variant="danger">{__("Delete")}</Button>
</DeleteCustomDomainDialog>
)}
</div>
</div>
</div>
</Card>
);
}

View File

@@ -1,97 +0,0 @@
import { useTranslate } from "@probo/i18n";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
Field,
useDialogRef,
IconTrashCan,
} from "@probo/ui";
import { useState, useEffect, type ReactNode } from "react";
import { sprintf } from "@probo/helpers";
interface DeleteCustomDomainDialogProps {
children: ReactNode;
domainName: string;
onConfirm: () => Promise<void>;
}
export function DeleteCustomDomainDialog({
children,
domainName,
onConfirm,
}: DeleteCustomDomainDialogProps) {
const { __ } = useTranslate();
const [inputValue, setInputValue] = useState("");
const [isDeleting, setIsDeleting] = useState(false);
const dialogRef = useDialogRef();
const isConfirmDisabled = inputValue !== domainName || isDeleting;
const handleConfirm = async () => {
if (inputValue === domainName && !isDeleting) {
setIsDeleting(true);
try {
await onConfirm();
dialogRef.current?.close();
} finally {
setIsDeleting(false);
}
}
};
useEffect(() => {
if (!isDeleting) {
setInputValue("");
}
}, [isDeleting]);
return (
<Dialog
className="max-w-lg"
ref={dialogRef}
trigger={children}
title={__("Delete Custom Domain")}
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{sprintf(
__(
"This will permanently delete the custom domain %s and all its configuration."
),
domainName
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
<Field
label={sprintf(
__('To confirm deletion, type "%s" below:'),
domainName
)}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={domainName}
disabled={isDeleting}
autoComplete="off"
autoFocus
/>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
icon={IconTrashCan}
onClick={handleConfirm}
disabled={isConfirmDisabled}
>
{isDeleting ? __("Deleting...") : __("Delete Domain")}
</Button>
</DialogFooter>
</Dialog>
);
}

View File

@@ -0,0 +1,237 @@
/**
* @generated SignedSource<<8bbf9386bee93021b7e06e5b3bd31c99>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MembershipRole = "ADMIN" | "AUDITOR" | "EMPLOYEE" | "OWNER" | "VIEWER";
export type domainSettingsPermissionsQuery$variables = {
organizationId: string;
};
export type domainSettingsPermissionsQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly viewerMembership: {
readonly role: MembershipRole;
};
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
readonly viewer: {
readonly canCreateCustomDomain: boolean;
readonly canDeleteCustomDomain: boolean;
};
};
export type domainSettingsPermissionsQuery = {
response: domainSettingsPermissionsQuery$data;
variables: domainSettingsPermissionsQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = {
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
},
v2 = {
"alias": "canCreateCustomDomain",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:custom-domain:create"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
v3 = {
"alias": "canDeleteCustomDomain",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:custom-domain:delete"
},
(v1/*: any*/)
],
"kind": "ScalarField",
"name": "permission",
"storageKey": null
},
v4 = [
(v1/*: any*/)
],
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "domainSettingsPermissionsQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
},
"action": "THROW"
},
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v6/*: any*/)
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "domainSettingsPermissionsQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v7/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v4/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v5/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Membership",
"kind": "LinkedField",
"name": "viewerMembership",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/)
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
},
(v7/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "de4faa8385c652071c57b4ddcc58e793",
"id": null,
"metadata": {},
"name": "domainSettingsPermissionsQuery",
"operationKind": "query",
"text": "query domainSettingsPermissionsQuery(\n $organizationId: ID!\n) {\n viewer {\n canCreateCustomDomain: permission(action: \"core:custom-domain:create\", id: $organizationId)\n canDeleteCustomDomain: permission(action: \"core:custom-domain:delete\", id: $organizationId)\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n viewerMembership {\n role\n id\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "75397f9db7cb6f2320ff91f30e773e22";
export default node;

View File

@@ -0,0 +1,67 @@
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { DomainSettingsPageQuery } from "./__generated__/DomainSettingsPageQuery.graphql";
import { useTranslate } from "@probo/i18n";
import { DomainCard } from "./_components/DomainCard";
import { NewDomainDialog } from "./_components/NewDomainDialog";
import { Button, Card, IconPlusLarge } from "@probo/ui";
export const domainSettingsPageQuery = graphql`
query DomainSettingsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
customDomain {
domain
...DomainCardFragment
}
}
}
}
`;
export function DomainSettingsPage(props: {
queryRef: PreloadedQuery<DomainSettingsPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const { organization } = usePreloadedQuery<DomainSettingsPageQuery>(
domainSettingsPageQuery,
queryRef,
);
if (organization.__typename !== "Organization") {
throw new Error("invalid type for node");
}
return (
<div className="space-y-4">
<h2 className="text-base font-medium">{__("Custom Domain")}</h2>
{organization.customDomain ? (
<DomainCard fKey={organization.customDomain} />
) : (
<Card padded>
<div className="text-center py-8">
<h3 className="text-lg font-semibold mb-2">
{__("No custom domain configured")}
</h3>
<p className="text-txt-tertiary mb-4">
{__(
"Add your own domain to make your trust center more professional",
)}
</p>
<div className="flex justify-center">
{/* {isAuthorized("Organization", "createCustomDomain") && ( */}
<NewDomainDialog>
<Button icon={IconPlusLarge}>{__("Add Domain")}</Button>
</NewDomainDialog>
{/* )} */}
</div>
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { Suspense, useEffect } from "react";
import { useOrganizationId } from "/hooks/useOrganizationId";
import {
DomainSettingsPage,
domainSettingsPageQuery,
} from "./DomainSettingsPage";
import { useQueryLoader } from "react-relay";
import { CoreRelayProvider } from "/providers/CoreRelayProvider";
import type { DomainSettingsPageQuery } from "./__generated__/DomainSettingsPageQuery.graphql";
function DomainSettingsPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<DomainSettingsPageQuery>(
domainSettingsPageQuery,
);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return null;
}
return <DomainSettingsPage queryRef={queryRef} />;
}
export default function () {
return (
<CoreRelayProvider>
<Suspense>
<DomainSettingsPageLoader />
</Suspense>
</CoreRelayProvider>
);
}

View File

@@ -1,46 +0,0 @@
import { useOutletContext } from "react-router";
import { useFragment, graphql } from "react-relay";
import { useTranslate } from "@probo/i18n";
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
import type { DomainSettingsTabFragment$key } from "./__generated__/DomainSettingsTabFragment.graphql";
const domainSettingsTabFragment = graphql`
fragment DomainSettingsTabFragment on Organization {
id
customDomain {
id
domain
sslStatus
dnsRecords {
type
name
value
ttl
purpose
}
createdAt
updatedAt
sslExpiresAt
}
}
`;
type OutletContext = {
organization: DomainSettingsTabFragment$key;
};
export default function DomainSettingsTab() {
const { __ } = useTranslate();
const { organization: organizationKey } = useOutletContext<OutletContext>();
const organization = useFragment(domainSettingsTabFragment, organizationKey);
return (
<div className="space-y-4">
<h2 className="text-base font-medium">{__("Custom Domain")}</h2>
<CustomDomainManager
organizationId={organization.id}
customDomain={organization.customDomain}
/>
</div>
);
}

View File

@@ -0,0 +1,247 @@
/**
* @generated SignedSource<<04a0aa6648637fd8e81b371ffd8ef9fb>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DomainSettingsPageQuery$variables = {
organizationId: string;
};
export type DomainSettingsPageQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly customDomain: {
readonly domain: string;
readonly " $fragmentSpreads": FragmentRefs<"DomainCardFragment">;
} | null | undefined;
readonly id: string;
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
};
export type DomainSettingsPageQuery = {
response: DomainSettingsPageQuery$data;
variables: DomainSettingsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domain",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DomainSettingsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "CustomDomain",
"kind": "LinkedField",
"name": "customDomain",
"plural": false,
"selections": [
(v4/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "DomainCardFragment"
}
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DomainSettingsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "CustomDomain",
"kind": "LinkedField",
"name": "customDomain",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslStatus",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "DNSRecordInstruction",
"kind": "LinkedField",
"name": "dnsRecords",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "value",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ttl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "purpose",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslExpiresAt",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "561dce9d969f951c2c5b0a2a5fecf29a",
"id": null,
"metadata": {},
"name": "DomainSettingsPageQuery",
"operationKind": "query",
"text": "query DomainSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n customDomain {\n domain\n ...DomainCardFragment\n id\n }\n }\n id\n }\n}\n\nfragment DomainCardFragment on CustomDomain {\n domain\n sslStatus\n ...DomainDialogFragment\n}\n\nfragment DomainDialogFragment on CustomDomain {\n sslStatus\n domain\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n}\n"
}
};
})();
(node as any).hash = "6b7bab65eca55ec2f132cd5944fe874e";
export default node;

View File

@@ -1,154 +0,0 @@
/**
* @generated SignedSource<<0e2aa976b1c9bb8dddf8b1dcc2f171d1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
import { FragmentRefs } from "relay-runtime";
export type DomainSettingsTabFragment$data = {
readonly customDomain: {
readonly createdAt: any;
readonly dnsRecords: ReadonlyArray<{
readonly name: string;
readonly purpose: string;
readonly ttl: number;
readonly type: string;
readonly value: string;
}>;
readonly domain: string;
readonly id: string;
readonly sslExpiresAt: any | null | undefined;
readonly sslStatus: SSLStatus;
readonly updatedAt: any;
} | null | undefined;
readonly id: string;
readonly " $fragmentType": "DomainSettingsTabFragment";
};
export type DomainSettingsTabFragment$key = {
readonly " $data"?: DomainSettingsTabFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DomainSettingsTabFragment",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "CustomDomain",
"kind": "LinkedField",
"name": "customDomain",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslStatus",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "DNSRecordInstruction",
"kind": "LinkedField",
"name": "dnsRecords",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "value",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ttl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "purpose",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslExpiresAt",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
};
})();
(node as any).hash = "00306efb96d302284155f5324ba2fb99";
export default node;

View File

@@ -0,0 +1,109 @@
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Button,
Dialog,
DialogContent,
DialogFooter,
Field,
IconTrashCan,
useDialogRef,
} from "@probo/ui";
import { useState, type PropsWithChildren } from "react";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { DeleteDomainDialogMutation } from "./__generated__/DeleteDomainDialogMutation.graphql";
const deleteCustomDomainMutation = graphql`
mutation DeleteDomainDialogMutation($input: DeleteCustomDomainInput!) {
deleteCustomDomain(input: $input) {
deletedCustomDomainId
}
}
`;
type DeleteDomainDialogProps = PropsWithChildren<{
domain: string;
}>;
export function DeleteDomainDialog(props: DeleteDomainDialogProps) {
const { children, domain } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const dialogRef = useDialogRef();
const [inputValue, setInputValue] = useState("");
const [deleteCustomDomain, isDeleting] =
useMutationWithToasts<DeleteDomainDialogMutation>(
deleteCustomDomainMutation,
{
successMessage: __("Domain deleted successfully"),
errorMessage: __("Failed to delete domain"),
},
);
const handleDeleteDomain = async () => {
return deleteCustomDomain({
variables: {
input: { organizationId },
},
onCompleted: () => {
dialogRef.current?.close();
},
updater: (store) => {
// Update the cache by setting customDomain to null
const organizationRecord = store.get(organizationId);
if (organizationRecord) {
organizationRecord.setValue(null, "customDomain");
}
},
});
};
return (
<Dialog
className="max-w-lg"
ref={dialogRef}
trigger={children}
title={__("Delete Custom Domain")}
>
<DialogContent padded className="space-y-4">
<p className="text-txt-secondary text-sm">
{sprintf(
__(
"This will permanently delete the custom domain %s and all its configuration.",
),
domain,
)}
</p>
<p className="text-red-600 text-sm font-medium">
{__("This action cannot be undone.")}
</p>
<Field
label={sprintf(__('To confirm deletion, type "%s" below:'), domain)}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={domain}
disabled={isDeleting}
autoComplete="off"
autoFocus
/>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
icon={IconTrashCan}
onClick={handleDeleteDomain}
disabled={isDeleting}
>
{isDeleting ? __("Deleting...") : __("Delete Domain")}
</Button>
</DialogFooter>
</Dialog>
);
}

View File

@@ -0,0 +1,63 @@
import { Badge, Button, Card } from "@probo/ui";
import { DomainDialog } from "./DomainDialog";
import {
getCustomDomainStatusBadgeLabel,
getCustomDomainStatusBadgeVariant,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import { useFragment } from "react-relay";
import type { DomainCardFragment$key } from "./__generated__/DomainCardFragment.graphql";
import { DeleteDomainDialog } from "./DeleteDomainDialog";
const fragment = graphql`
fragment DomainCardFragment on CustomDomain {
domain
sslStatus
...DomainDialogFragment
}
`;
export function DomainCard(props: { fKey: DomainCardFragment$key }) {
const { fKey } = props;
const { __ } = useTranslate();
const domain = useFragment<DomainCardFragment$key>(fragment, fKey);
return (
<Card>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div>
<div className="font-medium mb-1">{domain.domain}</div>
<div className="text-sm text-txt-secondary">
{domain.sslStatus === "ACTIVE"
? __("Verified")
: __("Pending verification")}
</div>
</div>
<Badge
variant={getCustomDomainStatusBadgeVariant(domain.sslStatus)}
>
{getCustomDomainStatusBadgeLabel(domain.sslStatus, __)}
</Badge>
</div>
<div className="flex items-center gap-2">
<DomainDialog fKey={domain}>
<Button variant="secondary">{__("View Details")}</Button>
</DomainDialog>
{/* {permissions.canDeleteCustomDomain && ( */}
<DeleteDomainDialog domain={domain.domain}>
<Button variant="danger">{__("Delete")}</Button>
</DeleteDomainDialog>
{/* )} */}
</div>
</div>
</div>
</Card>
);
}

View File

@@ -1,62 +1,46 @@
import {
getCustomDomainStatusBadgeLabel,
getCustomDomainStatusBadgeVariant,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Badge,
Button,
Dialog,
DialogContent,
Badge,
useDialogRef,
useToast,
} from "@probo/ui";
import type { ReactNode } from "react";
import type { PropsWithChildren } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { DomainDialogFragment$key } from "./__generated__/DomainDialogFragment.graphql";
interface DomainDetailsDialogProps {
children: ReactNode;
domain: {
readonly id: string;
readonly domain: string;
readonly sslStatus: string;
readonly dnsRecords?:
| readonly {
readonly type: string;
readonly name: string;
readonly value: string;
readonly ttl?: number;
readonly purpose: string;
}[]
| null;
readonly sslExpiresAt?: string | null;
};
}
const fragment = graphql`
fragment DomainDialogFragment on CustomDomain {
sslStatus
domain
dnsRecords {
type
name
value
ttl
purpose
}
createdAt
updatedAt
sslExpiresAt
}
`;
type DomainDialogProps = PropsWithChildren<{ fKey: DomainDialogFragment$key }>;
export function DomainDialog(props: DomainDialogProps) {
const { children, fKey } = props;
export function DomainDetailsDialog({
children,
domain,
}: DomainDetailsDialogProps) {
const { __ } = useTranslate();
const { toast } = useToast();
const dialogRef = useDialogRef();
const getStatusBadge = (domain: DomainDetailsDialogProps["domain"]) => {
if (domain.sslStatus === "ACTIVE") {
return <Badge variant="success">{__("Active")}</Badge>;
}
if (
domain.sslStatus === "PROVISIONING" ||
domain.sslStatus === "RENEWING"
) {
return <Badge variant="warning">{__("Provisioning")}</Badge>;
}
if (domain.sslStatus === "PENDING") {
return <Badge variant="warning">{__("Pending")}</Badge>;
}
if (domain.sslStatus === "FAILED") {
return <Badge variant="danger">{__("Failed")}</Badge>;
}
if (domain.sslStatus === "EXPIRED") {
return <Badge variant="danger">{__("Expired")}</Badge>;
}
return <Badge variant="neutral">{__("Unknown")}</Badge>;
};
const { toast } = useToast();
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
@@ -67,6 +51,8 @@ export function DomainDetailsDialog({
});
};
const domain = useFragment<DomainDialogFragment$key>(fragment, fKey);
return (
<Dialog
ref={dialogRef}
@@ -74,7 +60,9 @@ export function DomainDetailsDialog({
title={
<div className="flex items-center gap-3">
<span>{domain.domain}</span>
{getStatusBadge(domain)}
<Badge variant={getCustomDomainStatusBadgeVariant(domain.sslStatus)}>
{getCustomDomainStatusBadgeLabel(domain.sslStatus, __)}
</Badge>
</div>
}
>
@@ -83,7 +71,7 @@ export function DomainDetailsDialog({
<div className="bg-subtle rounded-lg p-4">
<div className="flex items-start">
<svg
className="w-5 h-5 text-green-500 mt-0.5 mr-3 flex-shrink-0"
className="w-5 h-5 text-green-500 mt-0.5 mr-3 shrink-0"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
@@ -97,7 +85,7 @@ export function DomainDetailsDialog({
<p className="font-medium mb-1">{__("Domain is active")}</p>
<p className="text-sm text-txt-secondary">
{__(
"Your custom domain is verified and SSL certificate is active"
"Your custom domain is verified and SSL certificate is active",
)}
</p>
{domain.sslExpiresAt && (
@@ -114,7 +102,7 @@ export function DomainDetailsDialog({
<h4 className="font-medium mb-3">{__("DNS Configuration")}</h4>
<p className="text-sm text-txt-secondary mb-4">
{__(
"Add these DNS records to your domain to complete verification"
"Add these DNS records to your domain to complete verification",
)}
</p>
@@ -172,7 +160,7 @@ export function DomainDetailsDialog({
<div className="bg-subtle rounded-lg p-4 mt-4">
<p className="text-sm">
{__(
"After adding the DNS records, verification will happen automatically. This may take a few minutes to propagate."
"After adding the DNS records, verification will happen automatically. This may take a few minutes to propagate.",
)}
</p>
</div>

View File

@@ -1,22 +1,23 @@
import { useTranslate } from "@probo/i18n";
import { graphql } from "react-relay";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
DialogFooter,
Field,
useDialogRef,
Breadcrumb,
} from "@probo/ui";
import { z } from "zod";
import { graphql } from "relay-runtime";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { ReactNode } from "react";
import type { CreateCustomDomainDialogMutation } from "./__generated__/CreateCustomDomainDialogMutation.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { PropsWithChildren } from "react";
import type { NewDomainDialogMutation } from "./__generated__/NewDomainDialogMutation.graphql";
const createCustomDomainMutation = graphql`
mutation CreateCustomDomainDialogMutation($input: CreateCustomDomainInput!) {
mutation NewDomainDialogMutation($input: CreateCustomDomainInput!) {
createCustomDomain(input: $input) {
customDomain {
id
@@ -43,21 +44,16 @@ const schema = z.object({
.min(1, "Domain is required")
.regex(
/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i,
"Please enter a valid domain (e.g., compliance.example.com)"
"Please enter a valid domain (e.g., compliance.example.com)",
),
});
type FormData = z.infer<typeof schema>;
type CustomDomainFormData = z.infer<typeof schema>;
interface CreateCustomDomainDialogProps {
children: ReactNode;
organizationId: string;
}
export function NewDomainDialog(props: PropsWithChildren) {
const { children } = props;
export function CreateCustomDomainDialog({
children,
organizationId,
}: CreateCustomDomainDialogProps) {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const dialogRef = useDialogRef();
@@ -67,21 +63,18 @@ export function CreateCustomDomainDialog({
defaultValues: {
domain: "",
},
}
},
);
const [createCustomDomain, isCreating] =
useMutationWithToasts<CreateCustomDomainDialogMutation>(
createCustomDomainMutation,
{
successMessage: __(
"Domain added successfully. Configure the DNS records to verify and activate your domain."
),
errorMessage: __("Failed to add domain"),
}
);
useMutationWithToasts<NewDomainDialogMutation>(createCustomDomainMutation, {
successMessage: __(
"Domain added successfully. Configure the DNS records to verify and activate your domain.",
),
errorMessage: __("Failed to add domain"),
});
const onSubmit = handleSubmit(async (data: FormData) => {
const onSubmit = handleSubmit(async (data: CustomDomainFormData) => {
const normalizedDomain = data.domain
.trim()
.toLowerCase()
@@ -100,12 +93,12 @@ export function CreateCustomDomainDialog({
const organizationRecord = store.get(organizationId);
if (organizationRecord && data?.createCustomDomain?.customDomain) {
const customDomainRecord = store.get(
data.createCustomDomain.customDomain.id
data.createCustomDomain.customDomain.id,
);
if (customDomainRecord) {
organizationRecord.setLinkedRecord(
customDomainRecord,
"customDomain"
"customDomain",
);
}
}
@@ -128,7 +121,7 @@ export function CreateCustomDomainDialog({
<div>
<p className="text-sm text-txt-secondary mb-4">
{__(
"Enter your domain and we'll generate the DNS records you need to add"
"Enter your domain and we'll generate the DNS records you need to add",
)}
</p>
</div>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b25e2b88094ba632cc451508af4df1e3>>
* @generated SignedSource<<4f27f16c09a86fda0f17b4d33866fcb2>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,17 +12,17 @@ import { ConcreteRequest } from 'relay-runtime';
export type DeleteCustomDomainInput = {
organizationId: string;
};
export type CustomDomainManagerDeleteMutation$variables = {
export type DeleteDomainDialogMutation$variables = {
input: DeleteCustomDomainInput;
};
export type CustomDomainManagerDeleteMutation$data = {
export type DeleteDomainDialogMutation$data = {
readonly deleteCustomDomain: {
readonly deletedCustomDomainId: string;
};
};
export type CustomDomainManagerDeleteMutation = {
response: CustomDomainManagerDeleteMutation$data;
variables: CustomDomainManagerDeleteMutation$variables;
export type DeleteDomainDialogMutation = {
response: DeleteDomainDialogMutation$data;
variables: DeleteDomainDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -64,7 +64,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CustomDomainManagerDeleteMutation",
"name": "DeleteDomainDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -73,20 +73,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CustomDomainManagerDeleteMutation",
"name": "DeleteDomainDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "9f4a3ed02de61bc60a8370e3771ca7bd",
"cacheID": "68e7fb0109af8d99c9947e5ef2314ad2",
"id": null,
"metadata": {},
"name": "CustomDomainManagerDeleteMutation",
"name": "DeleteDomainDialogMutation",
"operationKind": "mutation",
"text": "mutation CustomDomainManagerDeleteMutation(\n $input: DeleteCustomDomainInput!\n) {\n deleteCustomDomain(input: $input) {\n deletedCustomDomainId\n }\n}\n"
"text": "mutation DeleteDomainDialogMutation(\n $input: DeleteCustomDomainInput!\n) {\n deleteCustomDomain(input: $input) {\n deletedCustomDomainId\n }\n}\n"
}
};
})();
(node as any).hash = "e3878d11e361c3da2471363664150f3d";
(node as any).hash = "4ff39445b000bd5416e04e1460f11571";
export default node;

View File

@@ -0,0 +1,57 @@
/**
* @generated SignedSource<<efdbeab080fabb165a3a169fbedf3d8f>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
import { FragmentRefs } from "relay-runtime";
export type DomainCardFragment$data = {
readonly domain: string;
readonly sslStatus: SSLStatus;
readonly " $fragmentSpreads": FragmentRefs<"DomainDialogFragment">;
readonly " $fragmentType": "DomainCardFragment";
};
export type DomainCardFragment$key = {
readonly " $data"?: DomainCardFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DomainCardFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DomainCardFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslStatus",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "DomainDialogFragment"
}
],
"type": "CustomDomain",
"abstractKey": null
};
(node as any).hash = "672098f5351cf031d958263155e180ac";
export default node;

View File

@@ -0,0 +1,128 @@
/**
* @generated SignedSource<<1ee5bd8001e3d48b51c2869cd1ff5771>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
import { FragmentRefs } from "relay-runtime";
export type DomainDialogFragment$data = {
readonly createdAt: any;
readonly dnsRecords: ReadonlyArray<{
readonly name: string;
readonly purpose: string;
readonly ttl: number;
readonly type: string;
readonly value: string;
}>;
readonly domain: string;
readonly sslExpiresAt: any | null | undefined;
readonly sslStatus: SSLStatus;
readonly updatedAt: any;
readonly " $fragmentType": "DomainDialogFragment";
};
export type DomainDialogFragment$key = {
readonly " $data"?: DomainDialogFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DomainDialogFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DomainDialogFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslStatus",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domain",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "DNSRecordInstruction",
"kind": "LinkedField",
"name": "dnsRecords",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "value",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ttl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "purpose",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslExpiresAt",
"storageKey": null
}
],
"type": "CustomDomain",
"abstractKey": null
};
(node as any).hash = "0aec5615023dde2d23901fec9e43ed9b";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<c1db974cc1fa82299f39a71ece9bf7e3>>
* @generated SignedSource<<e47b433cc9fb58392ff933788bbe3deb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -14,10 +14,10 @@ export type CreateCustomDomainInput = {
domain: string;
organizationId: string;
};
export type CreateCustomDomainDialogMutation$variables = {
export type NewDomainDialogMutation$variables = {
input: CreateCustomDomainInput;
};
export type CreateCustomDomainDialogMutation$data = {
export type NewDomainDialogMutation$data = {
readonly createCustomDomain: {
readonly customDomain: {
readonly createdAt: any;
@@ -36,9 +36,9 @@ export type CreateCustomDomainDialogMutation$data = {
};
};
};
export type CreateCustomDomainDialogMutation = {
response: CreateCustomDomainDialogMutation$data;
variables: CreateCustomDomainDialogMutation$variables;
export type NewDomainDialogMutation = {
response: NewDomainDialogMutation$data;
variables: NewDomainDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -172,7 +172,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "CreateCustomDomainDialogMutation",
"name": "NewDomainDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -181,20 +181,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "CreateCustomDomainDialogMutation",
"name": "NewDomainDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "5e5bf90d17b1dc5129d6a0fc5d68d46c",
"cacheID": "b8aa27fe9ab3442dea9b96a70966524b",
"id": null,
"metadata": {},
"name": "CreateCustomDomainDialogMutation",
"name": "NewDomainDialogMutation",
"operationKind": "mutation",
"text": "mutation CreateCustomDomainDialogMutation(\n $input: CreateCustomDomainInput!\n) {\n createCustomDomain(input: $input) {\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n }\n}\n"
"text": "mutation NewDomainDialogMutation(\n $input: CreateCustomDomainInput!\n) {\n createCustomDomain(input: $input) {\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "fb3bede811b12db54f54af3dd375e335";
(node as any).hash = "f49cd22c5d6656e662fad5afab6fc344";
export default node;

View File

@@ -119,9 +119,7 @@ const routes = [
},
{
path: "documents/signing-requests",
Component: lazy(
() => import("./pages/DocumentSigningRequestsPage.tsx"),
),
Component: lazy(() => import("./pages/DocumentSigningRequestsPage")),
},
{
path: "api-keys",
@@ -170,7 +168,7 @@ const routes = [
{
path: "/organizations/:organizationId",
Component: lazy(
() => import("./pages/iam/memberships/MembershipLayoutLoader.tsx"),
() => import("./pages/iam/memberships/MembershipLayoutLoader"),
),
ErrorBoundary: ErrorBoundary,
children: [
@@ -192,7 +190,7 @@ const routes = [
path: "settings",
Fallback: PageSkeleton,
Component: lazy(
() => import("./pages/iam/organizations/settings/SettingsLayout.tsx"),
() => import("./pages/iam/organizations/settings/SettingsLayout"),
),
children: [
{
@@ -205,20 +203,21 @@ const routes = [
path: "general",
Component: lazy(
() =>
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx"),
import("./pages/iam/organizations/settings/GeneralSettingsPageLoader"),
),
},
{
path: "members",
Component: lazy(
() =>
import("./pages/iam/organizations/settings/MembersPageLoader.tsx"),
import("./pages/iam/organizations/settings/MembersPageLoader"),
),
},
{
path: "domain",
Component: lazy(
() => import("./pages/organizations/settings/DomainSettingsTab"),
() =>
import("./pages/organizations/settings/DomainSettingsPageLoader"),
),
},
{

View File

@@ -0,0 +1,45 @@
type Status =
| "ACTIVE"
| "PROVISIONING"
| "RENEWING"
| "PENDING"
| "FAILED"
| "EXPIRED";
export const getCustomDomainStatusBadgeVariant = (sslStatus: Status) => {
switch (sslStatus) {
case "ACTIVE":
return "success" as const;
case "PROVISIONING":
case "RENEWING":
case "PENDING":
return "warning" as const;
case "FAILED":
case "EXPIRED":
return "danger" as const;
default:
return "neutral" as const;
}
};
export const getCustomDomainStatusBadgeLabel = (
sslStatus: Status,
__: (key: string) => string,
) => {
if (sslStatus === "ACTIVE") {
return __("Active");
}
if (sslStatus === "PROVISIONING" || sslStatus === "RENEWING") {
return __("Provisioning");
}
if (sslStatus === "PENDING") {
return __("Pending");
}
if (sslStatus === "FAILED") {
return __("Failed");
}
if (sslStatus === "EXPIRED") {
return __("Expired");
}
return __("Unknown");
};