mirror of https://github.com/portainer/portainer
feat(system/upgrade): add get license dialog [EE-4743] (#8249)
parent
5942f4ff58
commit
406ff8812c
@ -0,0 +1,43 @@
|
||||
import { Meta, Story } from '@storybook/react';
|
||||
|
||||
import { Alert } from './Alert';
|
||||
|
||||
export default {
|
||||
component: Alert,
|
||||
title: 'Components/Alert',
|
||||
} as Meta;
|
||||
|
||||
interface Args {
|
||||
color: 'success' | 'error' | 'info';
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function Template({ text, color, title }: Args) {
|
||||
return (
|
||||
<Alert color={color} title={title}>
|
||||
{text}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
export const Success: Story<Args> = Template.bind({});
|
||||
Success.args = {
|
||||
color: 'success',
|
||||
title: 'Success',
|
||||
text: 'This is a success alert. Very long text, Very long text,Very long text ,Very long text ,Very long text, Very long text',
|
||||
};
|
||||
|
||||
export const Error: Story<Args> = Template.bind({});
|
||||
Error.args = {
|
||||
color: 'error',
|
||||
title: 'Error',
|
||||
text: 'This is an error alert',
|
||||
};
|
||||
|
||||
export const Info: Story<Args> = Template.bind({});
|
||||
Info.args = {
|
||||
color: 'info',
|
||||
title: 'Info',
|
||||
text: 'This is an info alert',
|
||||
};
|
@ -0,0 +1,83 @@
|
||||
import clsx from 'clsx';
|
||||
import { AlertCircle, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
type AlertType = 'success' | 'error' | 'info';
|
||||
|
||||
const alertSettings: Record<
|
||||
AlertType,
|
||||
{ container: string; header: string; body: string; icon: ReactNode }
|
||||
> = {
|
||||
success: {
|
||||
container:
|
||||
'border-green-4 bg-green-2 th-dark:bg-green-3 th-dark:border-green-5',
|
||||
header: 'text-green-8',
|
||||
body: 'text-green-7',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
error: {
|
||||
container:
|
||||
'border-error-4 bg-error-2 th-dark:bg-error-3 th-dark:border-error-5',
|
||||
header: 'text-error-8',
|
||||
body: 'text-error-7',
|
||||
icon: XCircle,
|
||||
},
|
||||
info: {
|
||||
container:
|
||||
'border-blue-4 bg-blue-2 th-dark:bg-blue-3 th-dark:border-blue-5',
|
||||
header: 'text-blue-8',
|
||||
body: 'text-blue-7',
|
||||
icon: AlertCircle,
|
||||
},
|
||||
};
|
||||
|
||||
export function Alert({
|
||||
color,
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<{ color: AlertType; title: string }>) {
|
||||
const { container, header, body, icon } = alertSettings[color];
|
||||
|
||||
return (
|
||||
<AlertContainer className={container}>
|
||||
<AlertHeader className={header}>
|
||||
<Icon icon={icon} />
|
||||
{title}
|
||||
</AlertHeader>
|
||||
<AlertBody className={body}>{children}</AlertBody>
|
||||
</AlertContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertContainer({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return (
|
||||
<div className={clsx('border-2 border-solid rounded-md', 'p-3', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertHeader({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return (
|
||||
<h4
|
||||
className={clsx('text-base', 'flex gap-2 items-center !m-0', className)}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertBody({
|
||||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return <div className={clsx('ml-6 mt-2 text-sm', className)}>{children}</div>;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { Alert } from './Alert';
|
@ -0,0 +1,112 @@
|
||||
import { ReactNode, useRef } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
let globalId = 0;
|
||||
|
||||
interface Props {
|
||||
portalId: HubSpotCreateFormOptions['portalId'];
|
||||
formId: HubSpotCreateFormOptions['formId'];
|
||||
region: HubSpotCreateFormOptions['region'];
|
||||
|
||||
onSubmitted: () => void;
|
||||
|
||||
loading?: ReactNode;
|
||||
}
|
||||
|
||||
export function HubspotForm({
|
||||
loading,
|
||||
portalId,
|
||||
region,
|
||||
formId,
|
||||
onSubmitted,
|
||||
}: Props) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const id = useRef(`reactHubspotForm${globalId++}`);
|
||||
const { isLoading } = useHubspotForm({
|
||||
elId: id.current,
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onSubmitted,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={elRef}
|
||||
id={id.current}
|
||||
style={{ display: isLoading ? 'none' : 'block' }}
|
||||
/>
|
||||
{isLoading && loading}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useHubspotForm({
|
||||
elId,
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onSubmitted,
|
||||
}: {
|
||||
elId: string;
|
||||
portalId: HubSpotCreateFormOptions['portalId'];
|
||||
formId: HubSpotCreateFormOptions['formId'];
|
||||
region: HubSpotCreateFormOptions['region'];
|
||||
|
||||
onSubmitted: () => void;
|
||||
}) {
|
||||
return useQuery(
|
||||
['hubspot', { elId, formId, portalId, region }],
|
||||
async () => {
|
||||
await loadHubspot();
|
||||
await createForm(`#${elId}`, {
|
||||
formId,
|
||||
portalId,
|
||||
region,
|
||||
onFormSubmit: onSubmitted,
|
||||
});
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function loadHubspot() {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (window.hbspt) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement(`script`);
|
||||
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
resolve();
|
||||
};
|
||||
script.src = `//js.hsforms.net/forms/v2.js`;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
async function createForm(
|
||||
target: string,
|
||||
options: Omit<HubSpotCreateFormOptions, 'target'>
|
||||
) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!window.hbspt) {
|
||||
throw new Error('hbspt object is missing');
|
||||
}
|
||||
|
||||
window.hbspt.forms.create({
|
||||
...options,
|
||||
target,
|
||||
onFormReady(...rest) {
|
||||
options.onFormReady?.(...rest);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import { HubspotForm } from '@@/HubspotForm';
|
||||
import { Modal } from '@@/modals/Modal';
|
||||
|
||||
export function GetLicenseDialog({
|
||||
onDismiss,
|
||||
goToUploadLicense,
|
||||
}: {
|
||||
onDismiss: () => void;
|
||||
goToUploadLicense: (isSubmitted: boolean) => void;
|
||||
}) {
|
||||
// form is loaded from hubspot, so it won't have the same styling as the rest of the app
|
||||
// since it won't support darkmode, we enforce a white background and black text for the components we use
|
||||
// (Modal, CloseButton, loading text)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onDismiss={onDismiss}
|
||||
aria-label="Upgrade Portainer to Business Edition"
|
||||
size="lg"
|
||||
className="!bg-white [&>.close-button]:!text-black"
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className="max-h-[80vh] overflow-auto">
|
||||
<HubspotForm
|
||||
region="na1"
|
||||
portalId="4731999"
|
||||
formId="1ef8ea88-3e03-46c5-8aef-c1d9f48fd06b"
|
||||
onSubmitted={() => goToUploadLicense(true)}
|
||||
loading={<div className="text-black">Loading...</div>}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue