@@ -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>
|
||||
);
|
||||
}
|
||||
45
common/resources/client/custom-domains/datatable/delete-domain-button.tsx
Executable file
45
common/resources/client/custom-domains/datatable/delete-domain-button.tsx
Executable file
@@ -0,0 +1,45 @@
|
||||
import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';
|
||||
import {Button} from '@common/ui/buttons/button';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';
|
||||
import React from 'react';
|
||||
import {useDeleteDomain} from '@common/custom-domains/datatable/requests/use-delete-domain';
|
||||
import {CustomDomain} from '@common/custom-domains/custom-domain';
|
||||
|
||||
interface DeleteDomainButtonProps {
|
||||
domain: CustomDomain;
|
||||
}
|
||||
export function DeleteDomainButton({domain}: DeleteDomainButtonProps) {
|
||||
const deleteDomain = useDeleteDomain();
|
||||
|
||||
return (
|
||||
<DialogTrigger
|
||||
type="modal"
|
||||
onClose={isConfirmed => {
|
||||
if (isConfirmed) {
|
||||
deleteDomain.mutate({domain});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="danger"
|
||||
size="xs"
|
||||
disabled={deleteDomain.isPending}
|
||||
>
|
||||
<Trans message="Remove" />
|
||||
</Button>
|
||||
<ConfirmationDialog
|
||||
title={<Trans message="Remove domain?" />}
|
||||
body={
|
||||
<Trans
|
||||
message="Are you sure you want to remove “:domain“?"
|
||||
values={{domain: domain.host}}
|
||||
/>
|
||||
}
|
||||
confirm={<Trans message="Remove" />}
|
||||
isDanger
|
||||
/>
|
||||
</DialogTrigger>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {CustomDomain} from '@common/custom-domains/custom-domain';
|
||||
import {ColumnConfig} from '@common/datatable/column-config';
|
||||
import {FormattedDate} from '@common/i18n/formatted-date';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {RemoteFavicon} from '@common/ui/remote-favicon';
|
||||
import React from 'react';
|
||||
import {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';
|
||||
import {BooleanIndicator} from '@common/datatable/column-templates/boolean-indicator';
|
||||
import {DeleteDomainButton} from '@common/custom-domains/datatable/delete-domain-button';
|
||||
|
||||
export const domainsDatatableColumns: ColumnConfig<CustomDomain>[] = [
|
||||
{
|
||||
key: 'host',
|
||||
allowsSorting: true,
|
||||
header: () => <Trans message="Domain" />,
|
||||
width: 'flex-3 min-w-200',
|
||||
visibleInMode: 'all',
|
||||
body: domain => (
|
||||
<div>
|
||||
<div className="flex items-center gap-6 whitespace-nowrap">
|
||||
<RemoteFavicon url={domain.host} />
|
||||
<a
|
||||
className="block font-semibold hover:underline overflow-ellipsis overflow-hidden w-min"
|
||||
href={domain.host}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-testid="host-name"
|
||||
>
|
||||
{domain.host}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'user_id',
|
||||
allowsSorting: true,
|
||||
header: () => <Trans message="Owner" />,
|
||||
width: 'flex-2 min-w-140',
|
||||
body: domain => {
|
||||
if (!domain.user) return '';
|
||||
return (
|
||||
<NameWithAvatar
|
||||
image={domain.user.avatar}
|
||||
label={domain.user.display_name}
|
||||
description={domain.user.email}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'global',
|
||||
allowsSorting: true,
|
||||
header: () => <Trans message="Global" />,
|
||||
body: domain => <BooleanIndicator value={domain.global} />,
|
||||
},
|
||||
{
|
||||
key: 'updated_at',
|
||||
allowsSorting: true,
|
||||
header: () => <Trans message="Last updated" />,
|
||||
body: domain =>
|
||||
domain.updated_at ? <FormattedDate date={domain.updated_at} /> : '',
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
header: () => <Trans message="Actions" />,
|
||||
hideHeader: true,
|
||||
width: 'w-80 flex-shrink-0',
|
||||
visibleInMode: 'all',
|
||||
align: 'end',
|
||||
body: domain => <DeleteDomainButton domain={domain} />,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
BackendFilter,
|
||||
FilterControlType,
|
||||
FilterOperator,
|
||||
} from '@common/datatable/filters/backend-filter';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {USER_MODEL} from '@common/auth/user';
|
||||
import {
|
||||
createdAtFilter,
|
||||
updatedAtFilter,
|
||||
} from '@common/datatable/filters/timestamp-filters';
|
||||
|
||||
export const DomainsDatatableFilters: BackendFilter[] = [
|
||||
{
|
||||
key: 'global',
|
||||
label: message('Global'),
|
||||
description: message('Whether domain is marked as global'),
|
||||
defaultOperator: FilterOperator.eq,
|
||||
control: {
|
||||
type: FilterControlType.BooleanToggle,
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
createdAtFilter({
|
||||
description: message('Date domain was created'),
|
||||
}),
|
||||
updatedAtFilter({
|
||||
description: message('Date domain was last updated'),
|
||||
}),
|
||||
{
|
||||
key: 'user_id',
|
||||
label: message('Owner'),
|
||||
description: message('User domain belongs to'),
|
||||
defaultOperator: FilterOperator.eq,
|
||||
control: {
|
||||
type: FilterControlType.SelectModel,
|
||||
model: USER_MODEL,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
import world from '@common/custom-domains/datatable/world.svg';
|
||||
import {Trans} from '@common/i18n/trans';
|
||||
import {
|
||||
DataTableEmptyStateMessage,
|
||||
DataTableEmptyStateMessageProps,
|
||||
} from '@common/datatable/page/data-table-emty-state-message';
|
||||
import React from 'react';
|
||||
|
||||
export function DomainsEmptyStateMessage(
|
||||
props: Partial<DataTableEmptyStateMessageProps>
|
||||
) {
|
||||
return (
|
||||
<DataTableEmptyStateMessage
|
||||
{...props}
|
||||
image={world}
|
||||
title={<Trans message="No domains have been connected yet" />}
|
||||
filteringTitle={<Trans message="No matching domains" />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {UseFormReturn} from 'react-hook-form';
|
||||
import {onFormQueryError} from '@common/errors/on-form-query-error';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
serverIp: string;
|
||||
}
|
||||
|
||||
export interface AuthorizeDomainConnectPayload {
|
||||
host: string;
|
||||
}
|
||||
|
||||
// check if is this host is not connected already and if user has permissions to connect domains
|
||||
export function useAuthorizeDomainConnect(
|
||||
form: UseFormReturn<AuthorizeDomainConnectPayload>,
|
||||
) {
|
||||
return useMutation({
|
||||
mutationFn: (props: AuthorizeDomainConnectPayload) => authorize(props),
|
||||
onError: err => onFormQueryError(err, form),
|
||||
});
|
||||
}
|
||||
|
||||
function authorize(payload: AuthorizeDomainConnectPayload): Promise<Response> {
|
||||
return apiClient
|
||||
.post('secure/custom-domain/authorize/store', payload)
|
||||
.then(r => r.data);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {toast} from '@common/ui/toast/toast';
|
||||
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
|
||||
import {CustomDomain} from '@common/custom-domains/custom-domain';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
domain: CustomDomain;
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
host: string;
|
||||
global?: boolean;
|
||||
}
|
||||
|
||||
export function useConnectDomain() {
|
||||
const {trans} = useTrans();
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (props: Payload) => connectDomain(props),
|
||||
onSuccess: response => {
|
||||
toast.positive(
|
||||
trans(
|
||||
message('“:domain” connected', {
|
||||
values: {domain: response.domain.host},
|
||||
}),
|
||||
),
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: DatatableDataQueryKey('custom-domain'),
|
||||
});
|
||||
},
|
||||
onError: err => showHttpErrorToast(err),
|
||||
});
|
||||
}
|
||||
|
||||
function connectDomain(payload: Payload): Promise<Response> {
|
||||
return apiClient.post('custom-domain', payload).then(r => r.data);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient, queryClient} from '@common/http/query-client';
|
||||
import {useTrans} from '@common/i18n/use-trans';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
|
||||
import {message} from '@common/i18n/message';
|
||||
import {toast} from '@common/ui/toast/toast';
|
||||
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
|
||||
import {CustomDomain} from '@common/custom-domains/custom-domain';
|
||||
import {removeProtocol} from '@common/utils/urls/remove-protocol';
|
||||
|
||||
interface Response extends BackendResponse {}
|
||||
|
||||
interface Payload {
|
||||
domain: CustomDomain;
|
||||
}
|
||||
|
||||
export function useDeleteDomain() {
|
||||
const {trans} = useTrans();
|
||||
return useMutation({
|
||||
mutationFn: (props: Payload) => deleteDomain(props),
|
||||
onSuccess: (response, props) => {
|
||||
toast.positive(
|
||||
trans(
|
||||
message('“:domain” removed', {
|
||||
values: {domain: removeProtocol(props.domain.host)},
|
||||
}),
|
||||
),
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: DatatableDataQueryKey('custom-domain'),
|
||||
});
|
||||
},
|
||||
onError: err => showHttpErrorToast(err),
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDomain({domain}: Payload): Promise<Response> {
|
||||
return apiClient.delete(`custom-domain/${domain.id}`).then(r => r.data);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient, queryClient} from '@common/http/query-client';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
import {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';
|
||||
import {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';
|
||||
import {CustomDomain} from '@common/custom-domains/custom-domain';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
domain: CustomDomain;
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
domainId: number | string;
|
||||
host?: string;
|
||||
global?: boolean;
|
||||
resource_id?: number | null;
|
||||
resource_type?: string | null;
|
||||
}
|
||||
|
||||
export function useUpdateDomain() {
|
||||
return useMutation({
|
||||
mutationFn: (props: Payload) => updateDomain(props),
|
||||
onSuccess: (response, props) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: DatatableDataQueryKey('custom-domain'),
|
||||
});
|
||||
},
|
||||
onError: err => showHttpErrorToast(err),
|
||||
});
|
||||
}
|
||||
|
||||
function updateDomain(payload: Payload): Promise<Response> {
|
||||
return apiClient
|
||||
.put(`custom-domain/${payload.domainId}`, payload)
|
||||
.then(r => r.data);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {useMutation} from '@tanstack/react-query';
|
||||
import {apiClient} from '@common/http/query-client';
|
||||
import {BackendResponse} from '@common/http/backend-response/backend-response';
|
||||
|
||||
interface Response extends BackendResponse {
|
||||
result: 'connected' | null;
|
||||
}
|
||||
|
||||
interface Payload {
|
||||
host: string;
|
||||
}
|
||||
|
||||
export function useValidateDomainDns() {
|
||||
return useMutation({
|
||||
mutationFn: (props: Payload) => authorize(props),
|
||||
});
|
||||
}
|
||||
|
||||
function authorize(payload: Payload): Promise<Response> {
|
||||
return apiClient
|
||||
.post('secure/custom-domain/validate/2BrM45vvfS/api', payload)
|
||||
.then(r => r.data);
|
||||
}
|
||||
1
common/resources/client/custom-domains/datatable/world.svg
Executable file
1
common/resources/client/custom-domains/datatable/world.svg
Executable file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 25 KiB |
Reference in New Issue
Block a user