mirror of https://github.com/portainer/portainer
feat(license): remove untrusted devices from node count [EE-5357] (#8817)
parent
5f6ddc2fad
commit
cfed481d6e
|
@ -2,13 +2,16 @@ package status
|
|||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
)
|
||||
|
||||
// NodesCount returns the total node number of all environments
|
||||
func NodesCount(endpoints []portainer.Endpoint) int {
|
||||
nodes := 0
|
||||
for _, env := range endpoints {
|
||||
nodes += countNodes(&env)
|
||||
if !endpointutils.IsEdgeEndpoint(&env) || env.UserTrusted {
|
||||
nodes += countNodes(&env)
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import clsx from 'clsx';
|
||||
import { AlertCircle, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { AlertCircle, AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
type AlertType = 'success' | 'error' | 'info';
|
||||
type AlertType = 'success' | 'error' | 'info' | 'warn';
|
||||
|
||||
const alertSettings: Record<
|
||||
AlertType,
|
||||
|
@ -31,22 +31,37 @@ const alertSettings: Record<
|
|||
body: 'text-blue-7',
|
||||
icon: AlertCircle,
|
||||
},
|
||||
warn: {
|
||||
container:
|
||||
'border-warning-4 bg-warning-2 th-dark:bg-warning-3 th-dark:border-warning-5',
|
||||
header: 'text-warning-8',
|
||||
body: 'text-warning-7',
|
||||
icon: AlertTriangle,
|
||||
},
|
||||
};
|
||||
|
||||
export function Alert({
|
||||
color,
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<{ color: AlertType; title: string }>) {
|
||||
}: 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>
|
||||
{title ? (
|
||||
<>
|
||||
<AlertHeader className={header}>
|
||||
<Icon icon={icon} />
|
||||
{title}
|
||||
</AlertHeader>
|
||||
<AlertBody className={body}>{children}</AlertBody>
|
||||
</>
|
||||
) : (
|
||||
<AlertBody className={clsx(body, 'flex items-center gap-2')}>
|
||||
<Icon icon={icon} /> {children}
|
||||
</AlertBody>
|
||||
)}
|
||||
</AlertContainer>
|
||||
);
|
||||
}
|
||||
|
@ -68,7 +83,11 @@ function AlertHeader({
|
|||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return (
|
||||
<h4
|
||||
className={clsx('text-base', '!m-0 flex items-center gap-2', className)}
|
||||
className={clsx(
|
||||
'text-base',
|
||||
'!m-0 mb-2 flex items-center gap-2',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</h4>
|
||||
|
@ -79,5 +98,5 @@ function AlertBody({
|
|||
className,
|
||||
children,
|
||||
}: PropsWithChildren<{ className?: string }>) {
|
||||
return <div className={clsx('ml-6 mt-2 text-sm', className)}>{children}</div>;
|
||||
return <div className={clsx('ml-6 text-sm', className)}>{children}</div>;
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ export function TooltipWithChildren({
|
|||
arrow
|
||||
allowHTML
|
||||
interactive
|
||||
disabled={!message}
|
||||
>
|
||||
{children}
|
||||
</Tippy>
|
||||
|
|
|
@ -6,12 +6,12 @@ import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/qu
|
|||
|
||||
import { Datatable as GenericDatatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { ModalType } from '@@/modals';
|
||||
import { TooltipWithChildren } from '@@/Tip/TooltipWithChildren';
|
||||
|
||||
import { useAssociateDeviceMutation, useLicenseOverused } from '../queries';
|
||||
|
||||
|
@ -26,7 +26,7 @@ const settingsStore = createPersistedStore(storageKey, 'Name');
|
|||
export function Datatable() {
|
||||
const associateMutation = useAssociateDeviceMutation();
|
||||
const removeMutation = useDeleteEnvironmentsMutation();
|
||||
const licenseOverused = useLicenseOverused();
|
||||
const { willExceed } = useLicenseOverused();
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
const { data: environments, totalCount, isLoading } = useEnvironments();
|
||||
|
||||
|
@ -41,28 +41,34 @@ export function Datatable() {
|
|||
<>
|
||||
<Button
|
||||
onClick={() => handleRemoveDevice(selectedRows)}
|
||||
disabled={selectedRows.length === 0 || licenseOverused}
|
||||
disabled={selectedRows.length === 0}
|
||||
color="dangerlight"
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove Device
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleAssociateDevice(selectedRows)}
|
||||
disabled={selectedRows.length === 0 || licenseOverused}
|
||||
<TooltipWithChildren
|
||||
message={
|
||||
willExceed(selectedRows.length) && (
|
||||
<>
|
||||
Associating devices is disabled as your node count exceeds
|
||||
your license limit
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
Associate Device
|
||||
</Button>
|
||||
|
||||
{licenseOverused ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<TextTip color="orange">
|
||||
Associating devices is disabled as your node count exceeds your
|
||||
license limit
|
||||
</TextTip>
|
||||
</div>
|
||||
) : null}
|
||||
<span>
|
||||
<Button
|
||||
onClick={() => handleAssociateDevice(selectedRows)}
|
||||
disabled={
|
||||
selectedRows.length === 0 || willExceed(selectedRows.length)
|
||||
}
|
||||
>
|
||||
Associate Device
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipWithChildren>
|
||||
</>
|
||||
)}
|
||||
isLoading={isLoading}
|
||||
|
|
|
@ -3,12 +3,17 @@ import { withLimitToBE } from '@/react/hooks/useLimitToBE';
|
|||
import { InformationPanel } from '@@/InformationPanel';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Link } from '@@/Link';
|
||||
import { Alert } from '@@/Alert';
|
||||
|
||||
import { Datatable } from './Datatable';
|
||||
import { useLicenseOverused, useUntrustedCount } from './queries';
|
||||
|
||||
export default withLimitToBE(WaitingRoomView);
|
||||
|
||||
function WaitingRoomView() {
|
||||
const untrustedCount = useUntrustedCount();
|
||||
const { willExceed } = useLicenseOverused();
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
|
@ -27,6 +32,19 @@ function WaitingRoomView() {
|
|||
</TextTip>
|
||||
</InformationPanel>
|
||||
|
||||
{willExceed(untrustedCount) && (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Alert color="warn">
|
||||
Associating all nodes in waiting room will exceed the node limit
|
||||
of your current license. Go to{' '}
|
||||
<Link to="portainer.licenses">Licenses</Link> page to view the
|
||||
current usage.
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Datatable />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { useIntegratedLicenseInfo } from '@/react/portainer/licenses/use-license.service';
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||
|
||||
export function useAssociateDeviceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -35,8 +36,23 @@ async function associateDevice(environmentId: EnvironmentId) {
|
|||
|
||||
export function useLicenseOverused() {
|
||||
const integratedInfo = useIntegratedLicenseInfo();
|
||||
if (integratedInfo && integratedInfo.licenseInfo.enforcedAt > 0) {
|
||||
return true;
|
||||
return {
|
||||
willExceed,
|
||||
isOverused: willExceed(0),
|
||||
};
|
||||
|
||||
function willExceed(moreNodes: number) {
|
||||
return (
|
||||
!!integratedInfo &&
|
||||
integratedInfo.usedNodes + moreNodes >= integratedInfo.licenseInfo.nodes
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useUntrustedCount() {
|
||||
const query = useEnvironmentList({
|
||||
edgeDeviceUntrusted: true,
|
||||
types: EdgeTypes,
|
||||
});
|
||||
return query.totalCount;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L66-L74
|
||||
// matches https://github.com/portainer/liblicense/blob/develop/liblicense.go#L66-L74
|
||||
export enum Edition {
|
||||
CE = 1,
|
||||
BE,
|
||||
EE,
|
||||
}
|
||||
|
||||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L60-L64
|
||||
// matches https://github.com/portainer/liblicense/blob/develop/liblicense.go#L64-L69
|
||||
|
||||
export enum LicenseType {
|
||||
Trial = 1,
|
||||
Subscription,
|
||||
/**
|
||||
* Essentials is the free 5-node license type
|
||||
*/
|
||||
Essentials,
|
||||
}
|
||||
|
||||
// matches https://github.com/portainer/liblicense/blob/master/liblicense.go#L35-L50
|
||||
// matches https://github.com/portainer/liblicense/blob/develop/liblicense.go#L35-L50
|
||||
export interface License {
|
||||
id: string;
|
||||
company: string;
|
||||
|
|
Loading…
Reference in New Issue