diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts index 5151d263e..801592b79 100644 --- a/app/portainer/react/views/index.ts +++ b/app/portainer/react/views/index.ts @@ -32,8 +32,8 @@ export const viewsModule = angular ) .component( 'settingsEdgeCompute', - r2a(withReactQuery(withCurrentUser(EdgeComputeSettingsView)), [ - 'onSubmit', - 'settings', - ]) + r2a( + withUIRouter(withReactQuery(withCurrentUser(EdgeComputeSettingsView))), + ['onSubmit', 'settings'] + ) ).name; diff --git a/app/react/components/EdgeIndicator.test.tsx b/app/react/components/EdgeIndicator.test.tsx index 6bb45895e..4ecb68396 100644 --- a/app/react/components/EdgeIndicator.test.tsx +++ b/app/react/components/EdgeIndicator.test.tsx @@ -1,6 +1,5 @@ import { createMockEnvironment } from '@/react-tools/test-mocks'; import { renderWithQueryClient } from '@/react-tools/test-utils'; -import { rest, server } from '@/setup-tests/server'; import { EdgeIndicator } from './EdgeIndicator'; @@ -25,8 +24,6 @@ async function renderComponent( checkInInterval = 0, queryDate = 0 ) { - server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({})))); - const environment = createMockEnvironment(); environment.EdgeID = edgeId; diff --git a/app/react/edge/hooks/useHasHeartbeat.ts b/app/react/edge/hooks/useHasHeartbeat.ts index 91f17f498..e55f594e0 100644 --- a/app/react/edge/hooks/useHasHeartbeat.ts +++ b/app/react/edge/hooks/useHasHeartbeat.ts @@ -1,6 +1,6 @@ import { Environment } from '@/react/portainer/environments/types'; import { usePublicSettings } from '@/react/portainer/settings/queries'; -import { PublicSettingsViewModel } from '@/portainer/models/settings'; +import { PublicSettingsResponse } from '@/react/portainer/settings/types'; export function useHasHeartbeat(environment: Environment) { const associated = !!environment.EdgeID; @@ -30,7 +30,7 @@ export function useHasHeartbeat(environment: Environment) { function getCheckinInterval( environment: Environment, - settings: PublicSettingsViewModel + settings: PublicSettingsResponse ) { const asyncMode = environment.Edge.AsyncMode; diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton.tsx b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton.tsx new file mode 100644 index 000000000..6fed0a9d5 --- /dev/null +++ b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton.tsx @@ -0,0 +1,71 @@ +import { useRouter } from '@uirouter/react'; +import { Plus } from 'lucide-react'; + +import { promptAsync } from '@/portainer/services/modal.service/prompt'; + +import { Button } from '@@/buttons'; + +import { usePublicSettings } from '../../queries'; + +enum DeployType { + FDO = 'FDO', + MANUAL = 'MANUAL', +} + +export function AddDeviceButton() { + const router = useRouter(); + const isFDOEnabledQuery = usePublicSettings({ + select: (settings) => settings.IsFDOEnabled, + }); + const isFDOEnabled = !!isFDOEnabledQuery.data; + + return ( + + ); + + async function handleNewDeviceClick() { + const result = await getDeployType(); + + switch (result) { + case DeployType.FDO: + router.stateService.go('portainer.endpoints.importDevice'); + break; + case DeployType.MANUAL: + router.stateService.go('portainer.wizard.endpoints', { + edgeDevice: true, + }); + break; + default: + break; + } + } + + function getDeployType(): Promise { + if (!isFDOEnabled) { + return Promise.resolve(DeployType.MANUAL); + } + + return promptAsync({ + title: 'How would you like to add an Edge Device?', + inputType: 'radio', + inputOptions: [ + { + text: 'Provision bare-metal using Intel FDO', + value: DeployType.FDO, + }, + { + text: 'Deploy agent manually', + value: DeployType.MANUAL, + }, + ], + buttons: { + confirm: { + label: 'Confirm', + className: 'btn-primary', + }, + }, + }) as Promise; + } +} diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx index 6487add70..33794c408 100644 --- a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx +++ b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx @@ -13,12 +13,8 @@ import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; import { Settings } from '../types'; import { validationSchema } from './EdgeComputeSettings.validation'; - -export interface FormValues { - EdgeAgentCheckinInterval: number; - EnableEdgeComputeFeatures: boolean; - EnforceEdgeID: boolean; -} +import { FormValues } from './types'; +import { AddDeviceButton } from './AddDeviceButton'; interface Props { settings?: Settings; @@ -33,7 +29,16 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) { return (
- + + Edge Compute settings + {settings.EnableEdgeComputeFeatures && } + + } + /> + ({ +export function usePublicSettings({ enabled, select, onSuccess, }: { - select?: (settings: PublicSettingsViewModel) => T; + select?: (settings: PublicSettingsResponse) => T; enabled?: boolean; onSuccess?: (data: T) => void; } = {}) { - return useQuery(['settings', 'public'], () => getPublicSettings(), { + return useQuery(['settings', 'public'], getPublicSettings, { select, ...withError('Unable to retrieve public settings'), enabled, diff --git a/app/react/portainer/settings/settings.service.ts b/app/react/portainer/settings/settings.service.ts index 996133b56..4b10f1ee5 100644 --- a/app/react/portainer/settings/settings.service.ts +++ b/app/react/portainer/settings/settings.service.ts @@ -1,14 +1,13 @@ -import { PublicSettingsViewModel } from '@/portainer/models/settings'; import axios, { parseAxiosError } from '@/portainer/services/axios'; -import { DefaultRegistry, PublicSettingsResponse, Settings } from './types'; +import { PublicSettingsResponse, DefaultRegistry, Settings } from './types'; export async function getPublicSettings() { try { const { data } = await axios.get( buildUrl('public') ); - return new PublicSettingsViewModel(data); + return data; } catch (e) { throw parseAxiosError( e as Error, diff --git a/app/react/portainer/settings/types.ts b/app/react/portainer/settings/types.ts index f054dab05..d73152a8d 100644 --- a/app/react/portainer/settings/types.ts +++ b/app/react/portainer/settings/types.ts @@ -137,23 +137,65 @@ export interface Settings { }; } -export interface PublicSettingsResponse { - // URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string - LogoURL: string; - // Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth - AuthenticationMethod: AuthenticationMethod; - // Whether edge compute features are enabled - EnableEdgeComputeFeatures: boolean; - // Supported feature flags - Features: Record; - // The URL used for oauth login - OAuthLoginURI: string; - // The URL used for oauth logout - OAuthLogoutURI: string; - // Whether portainer internal auth view will be hidden - OAuthHideInternalAuth: boolean; - // Whether telemetry is enabled - EnableTelemetry: boolean; - // The expiry of a Kubeconfig - KubeconfigExpiry: string; +interface GlobalDeploymentOptions { + /** Hide manual deploy forms in portainer */ + hideAddWithForm: boolean; + /** Configure this per environment or globally */ + perEnvOverride: boolean; + /** Hide the web editor in the remaining visible forms */ + hideWebEditor: boolean; + /** Hide the file upload option in the remaining visible forms */ + hideFileUpload: boolean; +} + +export interface PublicSettingsResponse { + /** URL to a logo that will be displayed on the login page as well as on top of the sidebar. Will use default Portainer logo when value is empty string */ + LogoURL: string; + /** The content in plaintext used to display in the login page. Will hide when value is empty string (only on BE) */ + CustomLoginBanner: string; + /** Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth */ + AuthenticationMethod: AuthenticationMethod; + /** The minimum required length for a password of any user when using internal auth mode */ + RequiredPasswordLength: number; + /** Deployment options for encouraging deployment as code (only on BE) */ + GlobalDeploymentOptions: GlobalDeploymentOptions; + /** Show the Kompose build option (discontinued in 2.18) */ + ShowKomposeBuildOption: boolean; + /** Whether edge compute features are enabled */ + EnableEdgeComputeFeatures: boolean; + /** Supported feature flags */ + Features: { [key: Feature]: boolean }; + /** The URL used for oauth login */ + OAuthLoginURI: string; + /** The URL used for oauth logout */ + OAuthLogoutURI: string; + /** Whether portainer internal auth view will be hidden (only on BE) */ + OAuthHideInternalAuth: boolean; + /** Whether telemetry is enabled */ + EnableTelemetry: boolean; + /** The expiry of a Kubeconfig */ + KubeconfigExpiry: string; + /** Whether team sync is enabled */ + TeamSync: boolean; + /** Whether FDO is enabled */ + IsFDOEnabled: boolean; + /** Whether AMT is enabled */ + IsAMTEnabled: boolean; + + /** Whether to hide default registry (only on BE) */ + DefaultRegistry: { + Hide: boolean; + }; + Edge: { + /** Whether the device has been started in edge async mode */ + AsyncMode: boolean; + /** The ping interval for edge agent - used in edge async mode [seconds] */ + PingInterval: number; + /** The snapshot interval for edge agent - used in edge async mode [seconds] */ + SnapshotInterval: number; + /** The command list interval for edge agent - used in edge async mode [seconds] */ + CommandInterval: number; + /** The check in interval for edge agent (in seconds) - used in non async mode [seconds] */ + CheckinInterval: number; + }; } diff --git a/app/setup-tests/server-handlers.ts b/app/setup-tests/server-handlers.ts index 2ab9ca295..922ce7b11 100644 --- a/app/setup-tests/server-handlers.ts +++ b/app/setup-tests/server-handlers.ts @@ -74,7 +74,18 @@ export const handlers = [ }), rest.get>( '/api/settings/public', - (req, res, ctx) => res(ctx.json({})) + (req, res, ctx) => + res( + ctx.json({ + Edge: { + AsyncMode: false, + CheckinInterval: 60, + CommandInterval: 60, + PingInterval: 60, + SnapshotInterval: 60, + }, + }) + ) ), rest.get>( '/api/status',