feat(environments): add edge device [EE-4840] (#8246)

* feat(environments): add edge device [EE-4840]

fix [EE-4840]

* fix(home): fix tests
pull/6820/head
Chaim Lev-Ari 2023-01-10 21:30:49 +02:00 committed by GitHub
parent 6c193a8a45
commit baf9c3db0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 170 additions and 48 deletions

View File

@ -32,8 +32,8 @@ export const viewsModule = angular
) )
.component( .component(
'settingsEdgeCompute', 'settingsEdgeCompute',
r2a(withReactQuery(withCurrentUser(EdgeComputeSettingsView)), [ r2a(
'onSubmit', withUIRouter(withReactQuery(withCurrentUser(EdgeComputeSettingsView))),
'settings', ['onSubmit', 'settings']
]) )
).name; ).name;

View File

@ -1,6 +1,5 @@
import { createMockEnvironment } from '@/react-tools/test-mocks'; import { createMockEnvironment } from '@/react-tools/test-mocks';
import { renderWithQueryClient } from '@/react-tools/test-utils'; import { renderWithQueryClient } from '@/react-tools/test-utils';
import { rest, server } from '@/setup-tests/server';
import { EdgeIndicator } from './EdgeIndicator'; import { EdgeIndicator } from './EdgeIndicator';
@ -25,8 +24,6 @@ async function renderComponent(
checkInInterval = 0, checkInInterval = 0,
queryDate = 0 queryDate = 0
) { ) {
server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({}))));
const environment = createMockEnvironment(); const environment = createMockEnvironment();
environment.EdgeID = edgeId; environment.EdgeID = edgeId;

View File

@ -1,6 +1,6 @@
import { Environment } from '@/react/portainer/environments/types'; import { Environment } from '@/react/portainer/environments/types';
import { usePublicSettings } from '@/react/portainer/settings/queries'; 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) { export function useHasHeartbeat(environment: Environment) {
const associated = !!environment.EdgeID; const associated = !!environment.EdgeID;
@ -30,7 +30,7 @@ export function useHasHeartbeat(environment: Environment) {
function getCheckinInterval( function getCheckinInterval(
environment: Environment, environment: Environment,
settings: PublicSettingsViewModel settings: PublicSettingsResponse
) { ) {
const asyncMode = environment.Edge.AsyncMode; const asyncMode = environment.Edge.AsyncMode;

View File

@ -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>;
}
}

View File

@ -13,12 +13,8 @@ import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { Settings } from '../types'; import { Settings } from '../types';
import { validationSchema } from './EdgeComputeSettings.validation'; import { validationSchema } from './EdgeComputeSettings.validation';
import { FormValues } from './types';
export interface FormValues { import { AddDeviceButton } from './AddDeviceButton';
EdgeAgentCheckinInterval: number;
EnableEdgeComputeFeatures: boolean;
EnforceEdgeID: boolean;
}
interface Props { interface Props {
settings?: Settings; settings?: Settings;
@ -33,7 +29,16 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
return ( return (
<div className="row"> <div className="row">
<Widget> <Widget>
<WidgetTitle icon={Laptop} title="Edge Compute settings" /> <WidgetTitle
icon={Laptop}
title={
<>
<span className="mr-3">Edge Compute settings</span>
{settings.EnableEdgeComputeFeatures && <AddDeviceButton />}
</>
}
/>
<WidgetBody> <WidgetBody>
<Formik <Formik
initialValues={settings} initialValues={settings}

View File

@ -1,7 +1,5 @@
export interface Settings { export interface FormValues {
EdgeAgentCheckinInterval: number;
EnableEdgeComputeFeatures: boolean; EnableEdgeComputeFeatures: boolean;
TrustOnFirstConnect: boolean;
EnforceEdgeID: boolean; EnforceEdgeID: boolean;
EdgePortainerUrl: string; EdgeAgentCheckinInterval: number;
} }

View File

@ -5,7 +5,6 @@ import {
withError, withError,
withInvalidate, withInvalidate,
} from '@/react-tools/react-query'; } from '@/react-tools/react-query';
import { PublicSettingsViewModel } from '@/portainer/models/settings';
import { import {
getSettings, getSettings,
@ -13,18 +12,18 @@ import {
getPublicSettings, getPublicSettings,
updateDefaultRegistry, updateDefaultRegistry,
} from './settings.service'; } 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, enabled,
select, select,
onSuccess, onSuccess,
}: { }: {
select?: (settings: PublicSettingsViewModel) => T; select?: (settings: PublicSettingsResponse) => T;
enabled?: boolean; enabled?: boolean;
onSuccess?: (data: T) => void; onSuccess?: (data: T) => void;
} = {}) { } = {}) {
return useQuery(['settings', 'public'], () => getPublicSettings(), { return useQuery(['settings', 'public'], getPublicSettings, {
select, select,
...withError('Unable to retrieve public settings'), ...withError('Unable to retrieve public settings'),
enabled, enabled,

View File

@ -1,14 +1,13 @@
import { PublicSettingsViewModel } from '@/portainer/models/settings';
import axios, { parseAxiosError } from '@/portainer/services/axios'; import axios, { parseAxiosError } from '@/portainer/services/axios';
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types'; import { PublicSettingsResponse, DefaultRegistry, Settings } from './types';
export async function getPublicSettings() { export async function getPublicSettings() {
try { try {
const { data } = await axios.get<PublicSettingsResponse>( const { data } = await axios.get<PublicSettingsResponse>(
buildUrl('public') buildUrl('public')
); );
return new PublicSettingsViewModel(data); return data;
} catch (e) { } catch (e) {
throw parseAxiosError( throw parseAxiosError(
e as Error, e as Error,

View File

@ -137,23 +137,65 @@ export interface Settings {
}; };
} }
export interface PublicSettingsResponse { interface GlobalDeploymentOptions {
// 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 /** Hide manual deploy forms in portainer */
LogoURL: string; hideAddWithForm: boolean;
// Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth /** Configure this per environment or globally */
AuthenticationMethod: AuthenticationMethod; perEnvOverride: boolean;
// Whether edge compute features are enabled /** Hide the web editor in the remaining visible forms */
EnableEdgeComputeFeatures: boolean; hideWebEditor: boolean;
// Supported feature flags /** Hide the file upload option in the remaining visible forms */
Features: Record<string, boolean>; hideFileUpload: boolean;
// The URL used for oauth login }
OAuthLoginURI: string;
// The URL used for oauth logout export interface PublicSettingsResponse {
OAuthLogoutURI: string; /** 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 */
// Whether portainer internal auth view will be hidden LogoURL: string;
OAuthHideInternalAuth: boolean; /** The content in plaintext used to display in the login page. Will hide when value is empty string (only on BE) */
// Whether telemetry is enabled CustomLoginBanner: string;
EnableTelemetry: boolean; /** Active authentication method for the Portainer instance. Valid values are: 1 for internal, 2 for LDAP, or 3 for oauth */
// The expiry of a Kubeconfig AuthenticationMethod: AuthenticationMethod;
KubeconfigExpiry: string; /** 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;
};
} }

View File

@ -74,7 +74,18 @@ export const handlers = [
}), }),
rest.get<DefaultRequestBody, PathParams, Partial<PublicSettingsResponse>>( rest.get<DefaultRequestBody, PathParams, Partial<PublicSettingsResponse>>(
'/api/settings/public', '/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>>( rest.get<DefaultRequestBody, PathParams, Partial<StatusResponse>>(
'/api/status', '/api/status',