1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

feat(system): path to upgrade standalone to BE [EE-4071] (#8095)

This commit is contained in:
Chaim Lev-Ari 2022-12-11 08:58:22 +02:00 committed by GitHub
parent 756ac034ec
commit 5cbf52377d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1374 additions and 421 deletions

View file

@ -2,10 +2,8 @@ import { useState } from 'react';
import { Database, Hash, Server, Tag, Wrench } from 'lucide-react';
import { DialogOverlay } from '@reach/dialog';
import {
useStatus,
useVersionStatus,
} from '@/portainer/services/api/status.service';
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
import { Button } from '@@/buttons';
@ -13,7 +11,7 @@ import styles from './Footer.module.css';
export function BuildInfoModalButton() {
const [isBuildInfoVisible, setIsBuildInfoVisible] = useState(false);
const statusQuery = useStatus();
const statusQuery = useSystemStatus();
if (!statusQuery.data) {
return null;
@ -39,8 +37,8 @@ export function BuildInfoModalButton() {
}
function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
const versionQuery = useVersionStatus();
const statusQuery = useStatus();
const versionQuery = useSystemVersion();
const statusQuery = useSystemStatus();
if (!statusQuery.data || !versionQuery.data) {
return null;

View file

@ -23,15 +23,6 @@ function CEFooter() {
<span>Community Edition</span>
<BuildInfoModalButton />
<a
href="https://www.portainer.io/install-BE-now"
className="text-blue-6 font-medium"
target="_blank"
rel="noreferrer"
>
Upgrade
</a>
</FooterContent>
</div>
);

View file

@ -1,9 +1,8 @@
import { useQuery } from 'react-query';
import clsx from 'clsx';
import { DownloadCloud } from 'lucide-react';
import { getVersionStatus } from '@/portainer/services/api/status.service';
import { useUIState } from '@/react/hooks/useUIState';
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
import { Icon } from '@@/Icon';
@ -11,7 +10,7 @@ import styles from './UpdateNotifications.module.css';
export function UpdateNotification() {
const uiStateStore = useUIState();
const query = useUpdateNotification();
const query = useSystemVersion();
if (!query.data || !query.data.UpdateAvailable) {
return null;
@ -67,7 +66,3 @@ export function UpdateNotification() {
uiStateStore.dismissUpdateVersion(version);
}
}
function useUpdateNotification() {
return useQuery(['status', 'version'], () => getVersionStatus());
}

View file

@ -13,7 +13,7 @@ import { SidebarItem } from './SidebarItem';
import { Footer } from './Footer';
import { Header } from './Header';
import { SidebarProvider } from './useSidebarState';
import { UpgradeBEBanner } from './UpgradeBEBanner';
import { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
export function Sidebar() {
const { isAdmin, user } = useUser();
@ -31,7 +31,7 @@ export function Sidebar() {
/* in the future (when we remove r2a) this should wrap the whole app - to change root styles */
<SidebarProvider>
<div className={clsx(styles.root, 'sidebar flex flex-col')}>
<UpgradeBEBanner />
<UpgradeBEBannerWrapper />
<nav
className={clsx(
styles.nav,

View file

@ -1,63 +0,0 @@
import { ArrowRight } from 'lucide-react';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import {
useFeatureFlag,
FeatureFlag,
} from '@/react/portainer/feature-flags/useRedirectFeatureFlag';
import { useNodesCount } from '@/react/portainer/status/useNodesCount';
import { useSystemInfo } from '@/react/portainer/status/useSystemInfo';
import { useSidebarState } from './useSidebarState';
export function UpgradeBEBanner() {
const { data } = useFeatureFlag(FeatureFlag.BEUpgrade, { enabled: !isBE });
if (isBE || !data) {
return null;
}
return <Inner />;
}
function Inner() {
const { trackEvent } = useAnalytics();
const { isOpen } = useSidebarState();
const nodesCountQuery = useNodesCount();
const systemInfoQuery = useSystemInfo();
if (!nodesCountQuery.data || !systemInfoQuery.data) {
return null;
}
const nodesCount = nodesCountQuery.data;
const systemInfo = systemInfoQuery.data;
const metadata = {
upgrade: false,
nodeCount: nodesCount,
platform: systemInfo.platform,
edgeAgents: systemInfo.edgeAgents,
edgeDevices: systemInfo.edgeDevices,
agents: systemInfo.agents,
};
return (
<button
type="button"
className="border-0 bg-warning-5 text-warning-9 w-full min-h-[48px] h-12 font-semibold flex justify-center items-center gap-3"
onClick={handleClick}
>
{isOpen && <>Upgrade to Business Edition</>}
<ArrowRight className="text-lg lucide" />
</button>
);
function handleClick() {
trackEvent('portainer-upgrade-admin', {
category: 'portainer',
metadata,
});
}
}

View file

@ -0,0 +1,52 @@
import { Loader2 } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
import { Modal } from '@@/modals/Modal';
import { Icon } from '@@/Icon';
export function LoadingDialog() {
useWaitForServerStatus();
return (
<Modal aria-label="Upgrade Portainer to Business Edition">
<Modal.Body>
<div className="flex flex-col items-center justify-center w-full">
<Icon
icon={Loader2}
className="animate-spin-slow !text-8xl !text-blue-8"
aria-label="loading"
/>
<h1 className="!text-2xl">Upgrading Portainer...</h1>
<p className="text-center text-gray-6 text-xl">
Please wait while we upgrade your Portainer to Business Edition.
</p>
</div>
</Modal.Body>
</Modal>
);
}
function useWaitForServerStatus() {
const [enabled, setEnabled] = useState(false);
useSystemStatus({
enabled,
retry: true,
onSuccess() {
window.location.reload();
},
});
useEffect(() => {
const timeoutId = setTimeout(() => {
setEnabled(true);
}, 3000);
return () => {
clearTimeout(timeoutId);
};
});
}

View file

@ -0,0 +1,52 @@
import { ExternalLink } from 'lucide-react';
import { Button } from '@@/buttons';
import { Modal } from '@@/modals/Modal';
import { ModalType } from '@@/modals/Modal/types';
export function NonAdminUpgradeDialog({
onDismiss,
}: {
onDismiss: () => void;
}) {
return (
<Modal aria-label="Upgrade Portainer to Business Edition">
<Modal.Header
title="Contact your administrator"
modalType={ModalType.Warn}
/>
<Modal.Body>
You need to be logged in as an admin to upgrade Portainer to Business
Edition.
</Modal.Body>
<Modal.Footer>
<div className="flex gap-2 w-full">
<Button
color="default"
size="medium"
className="w-1/3"
onClick={() => onDismiss()}
>
Cancel
</Button>
<a
href="https://www.portainer.io/take-5"
target="_blank"
rel="noreferrer"
className="no-link w-2/3"
>
<Button
color="primary"
size="medium"
className="w-full"
icon={ExternalLink}
>
Learn about Business Edition
</Button>
</a>
</div>
</Modal.Footer>
</Modal>
);
}

View file

@ -0,0 +1,78 @@
import { ArrowRight } from 'lucide-react';
import { useState } from 'react';
import { useAnalytics } from '@/angulartics.matomo/analytics-services';
import { useNodesCount } from '@/react/portainer/system/useNodesCount';
import {
ContainerPlatform,
useSystemInfo,
} from '@/react/portainer/system/useSystemInfo';
import { useUser } from '@/react/hooks/useUser';
import { withEdition } from '@/react/portainer/feature-flags/withEdition';
import { withHideOnExtension } from '@/react/hooks/withHideOnExtension';
import { useSidebarState } from '../useSidebarState';
import { UpgradeDialog } from './UpgradeDialog';
export const UpgradeBEBannerWrapper = withHideOnExtension(
withEdition(UpgradeBEBanner, 'CE')
);
const enabledPlatforms: Array<ContainerPlatform> = ['Docker Standalone'];
function UpgradeBEBanner() {
const { isAdmin } = useUser();
const { trackEvent } = useAnalytics();
const { isOpen: isSidebarOpen } = useSidebarState();
const nodesCountQuery = useNodesCount();
const systemInfoQuery = useSystemInfo();
const [isOpen, setIsOpen] = useState(false);
if (!nodesCountQuery.isSuccess || !systemInfoQuery.data) {
return null;
}
const nodesCount = nodesCountQuery.data;
const systemInfo = systemInfoQuery.data;
const metadata = {
upgrade: false,
nodeCount: nodesCount,
platform: systemInfo.platform,
edgeAgents: systemInfo.edgeAgents,
edgeDevices: systemInfo.edgeDevices,
agents: systemInfo.agents,
};
if (!enabledPlatforms.includes(systemInfo.platform)) {
return null;
}
return (
<>
<button
type="button"
className="border-0 bg-warning-5 text-warning-9 w-full min-h-[48px] h-12 font-semibold flex justify-center items-center gap-3"
onClick={handleClick}
>
{isSidebarOpen && <>Upgrade to Business Edition</>}
<ArrowRight className="text-lg lucide" />
</button>
{isOpen && <UpgradeDialog onDismiss={() => setIsOpen(false)} />}
</>
);
function handleClick() {
trackEvent(
isAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
{
category: 'portainer',
metadata,
}
);
setIsOpen(true);
}
}

View file

@ -0,0 +1,41 @@
import { useState } from 'react';
import { useUser } from '@/react/hooks/useUser';
import { UploadLicenseDialog } from './UploadLicenseDialog';
import { LoadingDialog } from './LoadingDialog';
import { NonAdminUpgradeDialog } from './NonAdminUpgradeDialog';
type Step = 'uploadLicense' | 'loading' | 'getLicense';
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
const { isAdmin } = useUser();
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
const component = getDialog();
return component;
function getDialog() {
if (!isAdmin) {
return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
}
switch (currentStep) {
case 'getLicense':
throw new Error('Not implemented');
// return <GetLicense setCurrentStep={setCurrentStep} />;
case 'uploadLicense':
return (
<UploadLicenseDialog
goToLoading={() => setCurrentStep('loading')}
onDismiss={onDismiss}
/>
);
case 'loading':
return <LoadingDialog />;
default:
throw new Error('step type not found');
}
}
}

View file

@ -0,0 +1,107 @@
import { Field, Form, Formik } from 'formik';
import { object, SchemaOf, string } from 'yup';
import { ExternalLink } from 'lucide-react';
import { useUpgradeEditionMutation } from '@/react/portainer/system/useUpgradeEditionMutation';
import { notifySuccess } from '@/portainer/services/notifications';
import { Button, LoadingButton } from '@@/buttons';
import { FormControl } from '@@/form-components/FormControl';
import { Input } from '@@/form-components/Input';
import { Modal } from '@@/modals/Modal';
interface FormValues {
license: string;
}
const initialValues: FormValues = {
license: '',
};
export function UploadLicenseDialog({
onDismiss,
goToLoading,
}: {
onDismiss: () => void;
goToLoading: () => void;
}) {
const upgradeMutation = useUpgradeEditionMutation();
return (
<Modal
onDismiss={onDismiss}
aria-label="Upgrade Portainer to Business Edition"
>
<Modal.Header
title={<h4 className="font-medium text-xl">Upgrade Portainer</h4>}
/>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validation}
>
{({ errors }) => (
<Form noValidate>
<Modal.Body>
<p className="font-semibold text-gray-7">
Please enter your Portainer License Below
</p>
<FormControl
label="License"
errors={errors.license}
required
size="vertical"
>
<Field name="license" as={Input} required />
</FormControl>
</Modal.Body>
<Modal.Footer>
<div className="flex gap-2 [&>*]:w-1/2 w-full">
<a
href="https://www.portainer.io/take-5"
target="_blank"
rel="noreferrer"
className="no-link"
>
<Button
color="default"
size="medium"
className="w-full"
icon={ExternalLink}
>
Get a license
</Button>
</a>
<LoadingButton
color="primary"
size="medium"
loadingText="Validating License"
isLoading={upgradeMutation.isLoading}
>
Start upgrade
</LoadingButton>
</div>
</Modal.Footer>
</Form>
)}
</Formik>
</Modal>
);
function handleSubmit(values: FormValues) {
upgradeMutation.mutate(values, {
onSuccess() {
notifySuccess('Starting upgrade', 'License validated successfully');
goToLoading();
},
});
}
}
function validation(): SchemaOf<FormValues> {
return object().shape({
license: string()
.required('License is required')
.matches(/^\d-.+/, 'License is invalid'),
});
}

View file

@ -0,0 +1 @@
export { UpgradeBEBannerWrapper } from './UpgradeBEBanner';