@@ -0,0 +1,100 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Dialog} from '@common/ui/overlays/dialog/dialog';
|
||||
import {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';
|
||||
import {DialogBody} from '@common/ui/overlays/dialog/dialog-body';
|
||||
import {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';
|
||||
import {KeyboardArrowRightIcon} from '@common/icons/material/KeyboardArrowRight';
|
||||
import {InfoStep} from '@common/custom-domains/datatable/connect-domain-dialog/info-step';
|
||||
import {HostStep} from '@common/custom-domains/datatable/connect-domain-dialog/host-step';
|
||||
import {ConnectDomainStep} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-step';
|
||||
import {BackendErrorResponse} from '@common/errors/backend-error-response';
|
||||
import {ValidationFailedStep} from '@common/custom-domains/datatable/connect-domain-dialog/validation-failed-step';
|
||||
import {useConnectDomainStepper} from '@common/custom-domains/datatable/connect-domain-dialog/use-connect-domain-stepper';
|
||||
import {Form} from '@common/ui/forms/form';
|
||||
import {FinalizeStep} from '@common/custom-domains/datatable/connect-domain-dialog/finalize-step';
|
||||
import {KeyboardArrowLeftIcon} from '@common/icons/material/KeyboardArrowLeft';
|
||||
|
||||
export interface DomainValidationErrorResponse extends BackendErrorResponse {
|
||||
failReason: 'serverNotConfigured' | 'dnsNotSetup';
|
||||
}
|
||||
|
||||
interface ConnectDomainDialogProps {
|
||||
showGlobalField?: boolean;
|
||||
}
|
||||
export function ConnectDomainDialog({
|
||||
showGlobalField,
|
||||
}: ConnectDomainDialogProps) {
|
||||
const {close, formId} = useDialogContext();
|
||||
const stepper = useConnectDomainStepper({showGlobalField});
|
||||
const StepComponent = getStepComponent(stepper.state.currentStep);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogHeader>
|
||||
<Trans message="Connect domain" />
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<Form
|
||||
form={stepper.form}
|
||||
id={formId}
|
||||
onSubmit={() => {
|
||||
stepper.goToNextStep();
|
||||
}}
|
||||
>
|
||||
<StepComponent stepper={stepper} />
|
||||
</Form>
|
||||
</DialogBody>
|
||||
<DialogFooter
|
||||
startAction={
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<Trans message="Cancel" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{stepper.hasPreviousStep && (
|
||||
<Button
|
||||
startIcon={<KeyboardArrowLeftIcon />}
|
||||
color="primary"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
stepper.goToPreviousStep();
|
||||
}}
|
||||
disabled={stepper.state.isLoading}
|
||||
>
|
||||
<Trans message="Previous" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="flat"
|
||||
color="primary"
|
||||
type="submit"
|
||||
form={formId}
|
||||
endIcon={<KeyboardArrowRightIcon />}
|
||||
disabled={stepper.state.isLoading}
|
||||
>
|
||||
<Trans message="Next" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function getStepComponent(step: ConnectDomainStep) {
|
||||
switch (step) {
|
||||
case ConnectDomainStep.Host:
|
||||
return HostStep;
|
||||
case ConnectDomainStep.Info:
|
||||
return InfoStep;
|
||||
case ConnectDomainStep.ValidationFailed:
|
||||
return ValidationFailedStep;
|
||||
case ConnectDomainStep.Finalize:
|
||||
return FinalizeStep;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {useConnectDomainStepper} from '@common/custom-domains/datatable/connect-domain-dialog/use-connect-domain-stepper';
|
||||
|
||||
export enum ConnectDomainStep {
|
||||
Host = 1,
|
||||
Info = 2,
|
||||
ValidationFailed = 3,
|
||||
Finalize = 4,
|
||||
}
|
||||
|
||||
export interface ConnectDomainStepProps {
|
||||
stepper: ReturnType<typeof useConnectDomainStepper>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {ProgressCircle} from '@common/ui/progress/progress-circle';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {ReactNode} from 'react';
|
||||
|
||||
interface DomainProgressIndicatorProps {
|
||||
message?: ReactNode;
|
||||
}
|
||||
export function DomainProgressIndicator({
|
||||
message = <Trans message="Checking DNS configuration..." />,
|
||||
}: DomainProgressIndicatorProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-18 text-base p-12 rounded bg-primary/10 text-primary">
|
||||
<ProgressCircle isIndeterminate size="sm" />
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {DomainProgressIndicator} from '@common/custom-domains/datatable/connect-domain-dialog/domain-progress-indicator';
|
||||
|
||||
export function FinalizeStep() {
|
||||
return (
|
||||
<div>
|
||||
<DomainProgressIndicator
|
||||
message={<Trans message="Connecting domain..." />}
|
||||
/>
|
||||
<div className="text-muted mt-10 text-xs">
|
||||
<Trans message="Don't close this window until domain is connected." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Fragment} from 'react';
|
||||
import {FormSwitch} from '@common/ui/forms/toggle/switch';
|
||||
import {ConnectDomainStepProps} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-step';
|
||||
|
||||
export function HostStep({stepper}: ConnectDomainStepProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormTextField
|
||||
autoFocus
|
||||
name="host"
|
||||
required
|
||||
maxLength={100}
|
||||
label={<Trans message="Host" />}
|
||||
placeholder="https://example.com"
|
||||
description={
|
||||
<Trans message="Enter the exact domain name you want your items to be accessible with. It can be a subdomain (example.yourdomain.com) or root domain (yourdomain.com)." />
|
||||
}
|
||||
/>
|
||||
{stepper.showGlobalField && (
|
||||
<FormSwitch
|
||||
className="mt-24 border-t pt-24"
|
||||
name="global"
|
||||
description={
|
||||
<Trans message="Whether all users should be able to select this domain." />
|
||||
}
|
||||
>
|
||||
<Trans message="Global" />
|
||||
</FormSwitch>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {useSettings} from '@common/core/settings/use-settings';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {ReactNode} from 'react';
|
||||
import {ConnectDomainStepProps} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-step';
|
||||
import {isSubdomain} from '@common/custom-domains/datatable/connect-domain-dialog/is-subdomain';
|
||||
import {DomainProgressIndicator} from '@common/custom-domains/datatable/connect-domain-dialog/domain-progress-indicator';
|
||||
|
||||
export function InfoStep({
|
||||
stepper: {
|
||||
state: {isLoading, host, serverIp},
|
||||
},
|
||||
}: ConnectDomainStepProps) {
|
||||
const {base_url} = useSettings();
|
||||
|
||||
if (isLoading) {
|
||||
return <DomainProgressIndicator />;
|
||||
}
|
||||
|
||||
if (isSubdomain(host)) {
|
||||
return (
|
||||
<Message
|
||||
title={
|
||||
<Trans message="Add this CNAME record to your domain by visiting your DNS provider or registrar." />
|
||||
}
|
||||
record="CNAME"
|
||||
target={base_url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Message
|
||||
title={
|
||||
<Trans message="Add this A record to your domain by visiting your DNS provider or registrar." />
|
||||
}
|
||||
record="A"
|
||||
target={serverIp}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface MessageProps {
|
||||
title: ReactNode;
|
||||
record: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
function Message({title, record, target}: MessageProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-muted mb-10">{title}</div>
|
||||
<div className="flex items-center gap-12 text-base p-12 rounded bg-primary/10 text-primary font-semibold">
|
||||
<div>{record}</div>
|
||||
{target}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isSubdomain(host: string): boolean {
|
||||
return (host.replace('www.', '').match(/\./g) || []).length > 1;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import {useState} from 'react';
|
||||
import {ConnectDomainStep} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-step';
|
||||
import {DomainValidationErrorResponse} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-dialog';
|
||||
import {
|
||||
AuthorizeDomainConnectPayload,
|
||||
useAuthorizeDomainConnect,
|
||||
} from '@common/custom-domains/datatable/requests/use-authorize-domain-connect';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {AxiosError} from 'axios';
|
||||
import {useValidateDomainDns} from '@common/custom-domains/datatable/requests/use-validate-domain-dns';
|
||||
import {useConnectDomain} from '@common/custom-domains/datatable/requests/use-connect-domain';
|
||||
import {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';
|
||||
|
||||
interface ConnectDomainStepperState {
|
||||
isLoading: boolean;
|
||||
currentStep: ConnectDomainStep;
|
||||
host: string;
|
||||
serverIp: string;
|
||||
validationFailReason?: DomainValidationErrorResponse['failReason'];
|
||||
}
|
||||
|
||||
interface StepHandlerResult {
|
||||
status: 'success' | 'error';
|
||||
newState?: Partial<ConnectDomainStepperState>;
|
||||
}
|
||||
|
||||
interface UseConnectDomainStepperProps {
|
||||
showGlobalField?: boolean;
|
||||
}
|
||||
export function useConnectDomainStepper({
|
||||
showGlobalField,
|
||||
}: UseConnectDomainStepperProps) {
|
||||
const {close} = useDialogContext();
|
||||
const form = useForm<AuthorizeDomainConnectPayload>();
|
||||
const authorizeDomainConnect = useAuthorizeDomainConnect(form);
|
||||
const validateDns = useValidateDomainDns();
|
||||
const connectDomain = useConnectDomain();
|
||||
|
||||
const [state, setState] = useState<ConnectDomainStepperState>({
|
||||
isLoading: false,
|
||||
currentStep: ConnectDomainStep.Host,
|
||||
host: '',
|
||||
serverIp: '',
|
||||
});
|
||||
|
||||
const startLoading = () => {
|
||||
setState({
|
||||
...state,
|
||||
isLoading: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDomainValidation = (): Promise<StepHandlerResult> => {
|
||||
return new Promise(resolve => {
|
||||
validateDns.mutate(
|
||||
{host: state.host},
|
||||
{
|
||||
onSuccess: () => {
|
||||
resolve({
|
||||
status: 'success',
|
||||
newState: {validationFailReason: undefined},
|
||||
});
|
||||
},
|
||||
onError: err => {
|
||||
resolve({
|
||||
status: 'error',
|
||||
newState: {
|
||||
validationFailReason: (
|
||||
err as AxiosError<DomainValidationErrorResponse>
|
||||
).response?.data.failReason,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDomainAuthorization = (): Promise<StepHandlerResult> => {
|
||||
return new Promise(resolve => {
|
||||
authorizeDomainConnect.mutate(form.getValues(), {
|
||||
onSuccess: response => {
|
||||
resolve({
|
||||
status: 'success',
|
||||
newState: {
|
||||
host: form.getValues().host,
|
||||
serverIp: response.serverIp,
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
resolve({status: 'error'});
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hasPreviousStep = state.currentStep !== ConnectDomainStep.Host;
|
||||
|
||||
const goToPreviousStep = () => {
|
||||
if (!hasPreviousStep || state.isLoading) return;
|
||||
|
||||
if (state.currentStep === ConnectDomainStep.Info) {
|
||||
setState({
|
||||
...state,
|
||||
currentStep: ConnectDomainStep.Host,
|
||||
});
|
||||
} else if (state.currentStep === ConnectDomainStep.ValidationFailed) {
|
||||
setState({
|
||||
...state,
|
||||
currentStep: ConnectDomainStep.Info,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextStep = async () => {
|
||||
if (state.currentStep === ConnectDomainStep.Host) {
|
||||
startLoading();
|
||||
const result = await handleDomainAuthorization();
|
||||
setState({
|
||||
...state,
|
||||
...result.newState,
|
||||
isLoading: false,
|
||||
currentStep:
|
||||
result.status === 'success'
|
||||
? ConnectDomainStep.Info
|
||||
: ConnectDomainStep.Host,
|
||||
});
|
||||
} else if (
|
||||
state.currentStep === ConnectDomainStep.Info ||
|
||||
state.currentStep === ConnectDomainStep.ValidationFailed
|
||||
) {
|
||||
startLoading();
|
||||
const validationResult = await handleDomainValidation();
|
||||
const nextStep =
|
||||
validationResult.status === 'success'
|
||||
? ConnectDomainStep.Finalize
|
||||
: ConnectDomainStep.ValidationFailed;
|
||||
setState({
|
||||
...state,
|
||||
...validationResult.newState,
|
||||
isLoading: false,
|
||||
currentStep: nextStep,
|
||||
});
|
||||
if (nextStep === ConnectDomainStep.Finalize) {
|
||||
connectDomain.mutate(form.getValues(), {
|
||||
onSettled: response => {
|
||||
close(response?.domain);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
state,
|
||||
goToNextStep,
|
||||
hasPreviousStep,
|
||||
goToPreviousStep,
|
||||
showGlobalField,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {useSettings} from '@common/core/settings/use-settings';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {Fragment, ReactNode} from 'react';
|
||||
import {ConnectDomainStepProps} from '@common/custom-domains/datatable/connect-domain-dialog/connect-domain-step';
|
||||
import {useAuth} from '@common/auth/use-auth';
|
||||
import {isSubdomain} from '@common/custom-domains/datatable/connect-domain-dialog/is-subdomain';
|
||||
import {WarningIcon} from '@common/icons/material/Warning';
|
||||
import {useValidateDomainDns} from '@common/custom-domains/datatable/requests/use-validate-domain-dns';
|
||||
import {DomainProgressIndicator} from '@common/custom-domains/datatable/connect-domain-dialog/domain-progress-indicator';
|
||||
|
||||
export function ValidationFailedStep({
|
||||
stepper: {
|
||||
goToNextStep,
|
||||
state: {host, serverIp, isLoading, validationFailReason},
|
||||
},
|
||||
}: ConnectDomainStepProps) {
|
||||
const validateDns = useValidateDomainDns();
|
||||
const {base_url} = useSettings();
|
||||
const {hasPermission} = useAuth();
|
||||
const subdomain = isSubdomain(host);
|
||||
const record = subdomain ? 'CNAME' : 'A';
|
||||
const location = subdomain ? base_url : serverIp;
|
||||
|
||||
if (isLoading) {
|
||||
return <DomainProgressIndicator />;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
validationFailReason === 'serverNotConfigured' && hasPermission('admin') ? (
|
||||
<ErrorMessage>
|
||||
<Trans
|
||||
message="DNS records for the domain are setup, however it seems that your server is not configured to handle requests from “:host“"
|
||||
values={{host: location}}
|
||||
/>
|
||||
</ErrorMessage>
|
||||
) : (
|
||||
<ErrorMessage>
|
||||
<Trans
|
||||
message="The domain is missing :record record pointing to :location or the changes haven't propagated yet."
|
||||
values={{record, location}}
|
||||
/>
|
||||
</ErrorMessage>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{errorMessage}
|
||||
<div className="whitespace-nowrap text-xs text-muted mt-10">
|
||||
<Trans
|
||||
message="You can wait and try again later, or <b>refresh</b>"
|
||||
values={{
|
||||
b: (text: string) => (
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
className="text-primary underline"
|
||||
onClick={() => {
|
||||
goToNextStep();
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
interface ErrorMessageProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
function ErrorMessage({children}: ErrorMessageProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-12 text-base p-12 rounded bg-warning/15 text-warning font-medium">
|
||||
<WarningIcon size="lg" />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user