diff --git a/api/internal/nodes/nodes.go b/api/internal/nodes/nodes.go
index 1e061cd7d..a95895f98 100644
--- a/api/internal/nodes/nodes.go
+++ b/api/internal/nodes/nodes.go
@@ -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
diff --git a/app/react/components/Alert/Alert.tsx b/app/react/components/Alert/Alert.tsx
index 46ea3b63c..6cbd101a6 100644
--- a/app/react/components/Alert/Alert.tsx
+++ b/app/react/components/Alert/Alert.tsx
@@ -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 (
-
-
- {title}
-
- {children}
+ {title ? (
+ <>
+
+
+ {title}
+
+ {children}
+ >
+ ) : (
+
+ {children}
+
+ )}
);
}
@@ -68,7 +83,11 @@ function AlertHeader({
}: PropsWithChildren<{ className?: string }>) {
return (
{children}
@@ -79,5 +98,5 @@ function AlertBody({
className,
children,
}: PropsWithChildren<{ className?: string }>) {
- return {children}
;
+ return {children}
;
}
diff --git a/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx
index a39ee4fa0..738d22a84 100644
--- a/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx
+++ b/app/react/components/Tip/TooltipWithChildren/TooltipWithChildren.tsx
@@ -69,6 +69,7 @@ export function TooltipWithChildren({
arrow
allowHTML
interactive
+ disabled={!message}
>
{children}
diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx
index 05e59fe83..04b96f9d8 100644
--- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx
+++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx
@@ -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() {
<>
handleRemoveDevice(selectedRows)}
- disabled={selectedRows.length === 0 || licenseOverused}
+ disabled={selectedRows.length === 0}
color="dangerlight"
icon={Trash2}
>
Remove Device
- handleAssociateDevice(selectedRows)}
- disabled={selectedRows.length === 0 || licenseOverused}
+
+ Associating devices is disabled as your node count exceeds
+ your license limit
+ >
+ )
+ }
>
- Associate Device
-
-
- {licenseOverused ? (
-
-
- Associating devices is disabled as your node count exceeds your
- license limit
-
-
- ) : null}
+
+ handleAssociateDevice(selectedRows)}
+ disabled={
+ selectedRows.length === 0 || willExceed(selectedRows.length)
+ }
+ >
+ Associate Device
+
+
+
>
)}
isLoading={isLoading}
diff --git a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx
index 396fb0a3a..aa2f0faca 100644
--- a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx
+++ b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx
@@ -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 (
<>
+ {willExceed(untrustedCount) && (
+
+
+
+ Associating all nodes in waiting room will exceed the node limit
+ of your current license. Go to{' '}
+ Licenses page to view the
+ current usage.
+
+
+
+ )}
+
>
);
diff --git a/app/react/edge/edge-devices/WaitingRoomView/queries.ts b/app/react/edge/edge-devices/WaitingRoomView/queries.ts
index f20d1d9c9..2ac4311de 100644
--- a/app/react/edge/edge-devices/WaitingRoomView/queries.ts
+++ b/app/react/edge/edge-devices/WaitingRoomView/queries.ts
@@ -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;
}
diff --git a/app/react/portainer/licenses/types.ts b/app/react/portainer/licenses/types.ts
index fac1d326c..f1b35a8ba 100644
--- a/app/react/portainer/licenses/types.ts
+++ b/app/react/portainer/licenses/types.ts
@@ -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;