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:
parent
756ac034ec
commit
5cbf52377d
73 changed files with 1374 additions and 421 deletions
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
52
app/react/sidebar/UpgradeBEBanner/LoadingDialog.tsx
Normal file
52
app/react/sidebar/UpgradeBEBanner/LoadingDialog.tsx
Normal 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);
|
||||
};
|
||||
});
|
||||
}
|
52
app/react/sidebar/UpgradeBEBanner/NonAdminUpgradeDialog.tsx
Normal file
52
app/react/sidebar/UpgradeBEBanner/NonAdminUpgradeDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
78
app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx
Normal file
78
app/react/sidebar/UpgradeBEBanner/UpgradeBEBanner.tsx
Normal 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);
|
||||
}
|
||||
}
|
41
app/react/sidebar/UpgradeBEBanner/UpgradeDialog.tsx
Normal file
41
app/react/sidebar/UpgradeBEBanner/UpgradeDialog.tsx
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
107
app/react/sidebar/UpgradeBEBanner/UploadLicenseDialog.tsx
Normal file
107
app/react/sidebar/UpgradeBEBanner/UploadLicenseDialog.tsx
Normal 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'),
|
||||
});
|
||||
}
|
1
app/react/sidebar/UpgradeBEBanner/index.ts
Normal file
1
app/react/sidebar/UpgradeBEBanner/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { UpgradeBEBannerWrapper } from './UpgradeBEBanner';
|
Loading…
Add table
Add a link
Reference in a new issue