feat(environments): add edge device [EE-4840] (#8246)
* feat(environments): add edge device [EE-4840] fix [EE-4840] * fix(home): fix testspull/6820/head
parent
6c193a8a45
commit
baf9c3db0a
app
portainer/react/views
react
components
edge/hooks
portainer/settings
EdgeComputeView/EdgeComputeSettings
setup-tests
|
@ -32,8 +32,8 @@ export const viewsModule = angular
|
|||
)
|
||||
.component(
|
||||
'settingsEdgeCompute',
|
||||
r2a(withReactQuery(withCurrentUser(EdgeComputeSettingsView)), [
|
||||
'onSubmit',
|
||||
'settings',
|
||||
])
|
||||
r2a(
|
||||
withUIRouter(withReactQuery(withCurrentUser(EdgeComputeSettingsView))),
|
||||
['onSubmit', 'settings']
|
||||
)
|
||||
).name;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<Button onClick={handleNewDeviceClick} icon={Plus}>
|
||||
Add Device
|
||||
</Button>
|
||||
);
|
||||
|
||||
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<DeployType> {
|
||||
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<DeployType>;
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon={Laptop} title="Edge Compute settings" />
|
||||
<WidgetTitle
|
||||
icon={Laptop}
|
||||
title={
|
||||
<>
|
||||
<span className="mr-3">Edge Compute settings</span>
|
||||
{settings.EnableEdgeComputeFeatures && <AddDeviceButton />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={settings}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
export interface Settings {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
export interface FormValues {
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
EdgeAgentCheckinInterval: number;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import {
|
|||
withError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
|
||||
import {
|
||||
getSettings,
|
||||
|
@ -13,18 +12,18 @@ import {
|
|||
getPublicSettings,
|
||||
updateDefaultRegistry,
|
||||
} from './settings.service';
|
||||
import { DefaultRegistry, Settings } from './types';
|
||||
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
|
||||
|
||||
export function usePublicSettings<T = PublicSettingsViewModel>({
|
||||
export function usePublicSettings<T = PublicSettingsResponse>({
|
||||
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,
|
||||
|
|
|
@ -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<PublicSettingsResponse>(
|
||||
buildUrl('public')
|
||||
);
|
||||
return new PublicSettingsViewModel(data);
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
|
|
|
@ -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<string, 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
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -74,7 +74,18 @@ export const handlers = [
|
|||
}),
|
||||
rest.get<DefaultRequestBody, PathParams, Partial<PublicSettingsResponse>>(
|
||||
'/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<DefaultRequestBody, PathParams, Partial<StatusResponse>>(
|
||||
'/api/status',
|
||||
|
|
Loading…
Reference in New Issue