mirror of https://github.com/buster-so/buster.git
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { Button } from '@/components/ui/buttons';
|
|
import { Input } from '@/components/ui/inputs';
|
|
import { Separator } from '@/components/ui/seperator';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { PulseLoader } from '@/components/ui/loaders';
|
|
import { useMemoizedFn } from '@/hooks';
|
|
import { createDayjsDate } from '@/lib/date';
|
|
import { BusterRoutes, createBusterRoute } from '@/routes';
|
|
import { ShareAssetType } from '@/api/asset_interfaces';
|
|
import { Text } from '@/components/ui/typography';
|
|
import { useBusterNotifications } from '@/context/BusterNotifications';
|
|
import { Link, Eye, EyeSlash } from '@/components/ui/icons';
|
|
import { DatePicker } from '@/components/ui/date';
|
|
import { useUpdateCollectionShare } from '@/api/buster_rest/collections';
|
|
import { useUpdateMetricShare } from '@/api/buster_rest/metrics';
|
|
import { useUpdateDashboardShare } from '@/api/buster_rest/dashboards';
|
|
import { SelectSingleEventHandler } from 'react-day-picker';
|
|
import { ShareMenuContentBodyProps } from './ShareMenuContentBody';
|
|
import { cn } from '@/lib/classMerge';
|
|
|
|
export const ShareMenuContentPublish: React.FC<ShareMenuContentBodyProps> = React.memo(
|
|
({
|
|
assetType,
|
|
assetId,
|
|
password = '',
|
|
publicly_accessible,
|
|
onCopyLink,
|
|
publicExpirationDate,
|
|
className
|
|
}) => {
|
|
const { openInfoMessage } = useBusterNotifications();
|
|
const { mutateAsync: onShareMetric, isPending: isPublishingMetric } = useUpdateMetricShare();
|
|
const { mutateAsync: onShareDashboard, isPending: isPublishingDashboard } =
|
|
useUpdateDashboardShare();
|
|
const { mutateAsync: onShareCollection, isPending: isPublishingCollection } =
|
|
useUpdateCollectionShare();
|
|
const [isPasswordProtected, setIsPasswordProtected] = useState<boolean>(!!password);
|
|
const [_password, _setPassword] = React.useState<string>(password || '');
|
|
|
|
const isPublishing = isPublishingMetric || isPublishingDashboard || isPublishingCollection;
|
|
|
|
const linkExpiry = useMemo(() => {
|
|
return publicExpirationDate ? new Date(publicExpirationDate) : null;
|
|
}, [publicExpirationDate]);
|
|
|
|
const url = useMemo(() => {
|
|
let url = '';
|
|
if (assetType === ShareAssetType.METRIC) {
|
|
url = createBusterRoute({ route: BusterRoutes.APP_METRIC_ID_CHART, metricId: assetId });
|
|
} else if (assetType === ShareAssetType.DASHBOARD) {
|
|
url = createBusterRoute({ route: BusterRoutes.APP_DASHBOARD_ID, dashboardId: assetId });
|
|
} else if (assetType === ShareAssetType.COLLECTION) {
|
|
url = createBusterRoute({ route: BusterRoutes.APP_COLLECTIONS });
|
|
}
|
|
return window.location.origin + url;
|
|
}, [assetId, assetType]);
|
|
|
|
const onTogglePublish = useMemoizedFn(async (v?: boolean) => {
|
|
const linkExp = linkExpiry ? linkExpiry.toISOString() : null;
|
|
const payload: Parameters<typeof onShareMetric>[0] = {
|
|
id: assetId,
|
|
params: {
|
|
publicly_accessible: v === undefined ? true : !!v,
|
|
public_password: _password || null,
|
|
public_expiry_date: linkExp
|
|
}
|
|
};
|
|
if (assetType === ShareAssetType.METRIC) {
|
|
await onShareMetric(payload);
|
|
} else if (assetType === ShareAssetType.DASHBOARD) {
|
|
await onShareDashboard(payload);
|
|
} else if (assetType === ShareAssetType.COLLECTION) {
|
|
await onShareCollection(payload);
|
|
}
|
|
});
|
|
|
|
const onSetPasswordProtected = useMemoizedFn(async (v: boolean) => {
|
|
onSetPassword(null);
|
|
setIsPasswordProtected(v);
|
|
});
|
|
|
|
const onSetPassword = useMemoizedFn(async (password: string | null) => {
|
|
const payload: Parameters<typeof onShareMetric>[0] = {
|
|
id: assetId,
|
|
params: {
|
|
public_password: password
|
|
}
|
|
};
|
|
|
|
if (assetType === ShareAssetType.METRIC) {
|
|
await onShareMetric(payload);
|
|
} else if (assetType === ShareAssetType.DASHBOARD) {
|
|
await onShareDashboard(payload);
|
|
} else if (assetType === ShareAssetType.COLLECTION) {
|
|
await onShareCollection(payload);
|
|
}
|
|
_setPassword(password || '');
|
|
if (password) openInfoMessage('Password updated');
|
|
});
|
|
|
|
const onSetExpirationDate = useMemoizedFn(async (date: Date | null) => {
|
|
const linkExp = date ? date.toISOString() : null;
|
|
|
|
const payload: Parameters<typeof onShareMetric>[0] = {
|
|
id: assetId,
|
|
params: {
|
|
public_expiry_date: linkExp
|
|
}
|
|
};
|
|
|
|
if (assetType === ShareAssetType.METRIC) {
|
|
await onShareMetric(payload);
|
|
} else if (assetType === ShareAssetType.DASHBOARD) {
|
|
await onShareDashboard(payload);
|
|
} else if (assetType === ShareAssetType.COLLECTION) {
|
|
await onShareCollection(payload);
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
_setPassword(password || '');
|
|
setIsPasswordProtected(!!password);
|
|
}, [password]);
|
|
|
|
return (
|
|
<div className="flex flex-col space-y-1">
|
|
<div className={cn('flex flex-col space-y-3', className)}>
|
|
{publicly_accessible ? (
|
|
<>
|
|
<IsPublishedInfo isPublished={publicly_accessible} />
|
|
|
|
<div className="flex w-full space-x-0.5">
|
|
<Input size="small" readOnly value={url} />
|
|
<Button variant="default" className="flex" prefix={<Link />} onClick={onCopyLink} />
|
|
</div>
|
|
|
|
<LinkExpiration linkExpiry={linkExpiry} onChangeLinkExpiry={onSetExpirationDate} />
|
|
|
|
<SetAPassword
|
|
password={_password}
|
|
onSetPassword={onSetPassword}
|
|
isPasswordProtected={isPasswordProtected}
|
|
onSetPasswordProtected={onSetPasswordProtected}
|
|
/>
|
|
</>
|
|
) : (
|
|
<div className="flex flex-col space-y-2.5">
|
|
<Text variant="secondary">Anyone with the link will be able to view.</Text>
|
|
|
|
<Button
|
|
loading={isPublishing}
|
|
onClick={() => {
|
|
onTogglePublish(true);
|
|
}}>
|
|
Create public link
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{publicly_accessible && (
|
|
<>
|
|
<div className={cn('flex justify-end space-x-2 border-t', className)}>
|
|
<Button
|
|
block
|
|
onClick={async (v) => {
|
|
onTogglePublish(false);
|
|
}}>
|
|
Unpublish
|
|
</Button>
|
|
<Button block onClick={onCopyLink}>
|
|
Copy link
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
ShareMenuContentPublish.displayName = 'ShareMenuContentPublish';
|
|
|
|
const IsPublishedInfo: React.FC<{ isPublished: boolean }> = React.memo(({ isPublished }) => {
|
|
if (!isPublished) return null;
|
|
|
|
return (
|
|
<div className="flex items-center space-x-2">
|
|
<PulseLoader />
|
|
<Text variant="link">Live on the web</Text>
|
|
</div>
|
|
);
|
|
});
|
|
IsPublishedInfo.displayName = 'IsPublishedInfo';
|
|
|
|
const LinkExpiration: React.FC<{
|
|
linkExpiry: Date | null;
|
|
onChangeLinkExpiry: (date: Date | null) => void;
|
|
}> = React.memo(({ onChangeLinkExpiry, linkExpiry }) => {
|
|
const dateFormat = 'LL';
|
|
|
|
const now = useMemo(() => {
|
|
return createDayjsDate(new Date());
|
|
}, []);
|
|
|
|
const maxDate = useMemo(() => {
|
|
return createDayjsDate(new Date()).add(2, 'year');
|
|
}, []);
|
|
|
|
const onSelect = useMemoizedFn((date: Date | undefined) => {
|
|
onChangeLinkExpiry(date || null);
|
|
});
|
|
|
|
return (
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<Text truncate>Link expiration</Text>
|
|
|
|
<DatePicker
|
|
selected={linkExpiry || undefined}
|
|
onSelect={onSelect}
|
|
mode="single"
|
|
dateFormat={dateFormat}
|
|
placeholder="Never"
|
|
disabled={(date) => {
|
|
const dateValue = createDayjsDate(date);
|
|
return dateValue.isBefore(now) || dateValue.isAfter(maxDate);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
});
|
|
LinkExpiration.displayName = 'LinkExpiration';
|
|
|
|
const SetAPassword: React.FC<{
|
|
password: string;
|
|
onSetPassword: (password: string | null) => void;
|
|
isPasswordProtected: boolean;
|
|
onSetPasswordProtected: (isPasswordProtected: boolean) => void;
|
|
}> = React.memo(
|
|
({ password: passwordProp, onSetPassword, isPasswordProtected, onSetPasswordProtected }) => {
|
|
const [visibilityToggle, setVisibilityToggle] = useState<boolean>(false);
|
|
const [password, setPassword] = useState<string>(passwordProp);
|
|
|
|
const isPasswordDifferent = password !== passwordProp;
|
|
|
|
const onChangeChecked = useMemoizedFn((checked: boolean) => {
|
|
onSetPasswordProtected(checked);
|
|
});
|
|
|
|
const onChangePassword = useMemoizedFn((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setPassword(e.target.value);
|
|
});
|
|
|
|
const onClickVisibilityToggle = useMemoizedFn(() => {
|
|
setVisibilityToggle(!visibilityToggle);
|
|
});
|
|
|
|
const onClickSave = useMemoizedFn(() => {
|
|
onSetPassword(password);
|
|
});
|
|
|
|
const memoizedVisibilityToggle = useMemo(() => {
|
|
return {
|
|
visible: visibilityToggle,
|
|
onVisibleChange: (visible: boolean) => setVisibilityToggle(visible)
|
|
};
|
|
}, [visibilityToggle]);
|
|
|
|
useEffect(() => {
|
|
if (isPasswordProtected) {
|
|
setPassword(password);
|
|
} else {
|
|
setPassword('');
|
|
}
|
|
}, [isPasswordProtected, password]);
|
|
|
|
return (
|
|
<div className="flex w-full flex-col space-y-3">
|
|
<div className="flex w-full justify-between">
|
|
<Text>Set a password</Text>
|
|
<Switch checked={isPasswordProtected} onCheckedChange={onChangeChecked} />
|
|
</div>
|
|
|
|
{isPasswordProtected && (
|
|
<div className="flex w-full items-center space-x-2">
|
|
<div className="flex w-full">
|
|
<div className="relative flex w-full space-x-0.5">
|
|
<Input
|
|
value={password}
|
|
onChange={onChangePassword}
|
|
placeholder="Password"
|
|
type={visibilityToggle ? 'text' : 'password'}
|
|
/>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="small"
|
|
className="absolute top-1/2 right-[7px] -translate-y-1/2"
|
|
prefix={!visibilityToggle ? <Eye /> : <EyeSlash />}
|
|
onClick={onClickVisibilityToggle}></Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Button disabled={!isPasswordDifferent} onClick={onClickSave}>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
SetAPassword.displayName = 'SetAPassword';
|