mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 22:05:23 +02:00
refactor(app): move settings components to react [EE-3442] (#7625)
This commit is contained in:
parent
5777c18297
commit
1e21961e6a
66 changed files with 488 additions and 96 deletions
|
@ -1,4 +1,4 @@
|
|||
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import { useInfo } from 'Docker/services/system.service';
|
||||
import { EnvironmentId } from 'Portainer/environments/types';
|
||||
|
||||
import { useInfo } from '@/docker/services/system.service';
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
|
||||
import { DockerContainer, ContainerStatus } from './types';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Fragment } from 'react';
|
||||
import DockerNetworkHelper from 'Docker/helpers/networkHelper';
|
||||
|
||||
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
|
||||
import { Authorized } from '@/portainer/hooks/useUser';
|
||||
|
||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
import {
|
||||
usePublicSettings,
|
||||
useUpdateDefaultRegistrySettingsMutation,
|
||||
} from 'Portainer/settings/queries';
|
||||
import { notifySuccess } from 'Portainer/services/notifications';
|
||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||
|
||||
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||
} from '@/react/portainer/settings/queries';
|
||||
|
||||
import { Tooltip } from '@@/Tip/Tooltip';
|
||||
import { Button } from '@@/buttons';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import clsx from 'clsx';
|
||||
import { usePublicSettings } from 'Portainer/settings/queries';
|
||||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
export function DefaultRegistryDomain() {
|
||||
const settingsQuery = usePublicSettings({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { usePublicSettings } from 'Portainer/settings/queries';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
export function DefaultRegistryName() {
|
||||
const settingsQuery = usePublicSettings({
|
||||
select: (settings) => settings.DefaultRegistry?.Hide,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export interface Props {
|
||||
value: string;
|
||||
icon?: ReactNode;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// Helper function used as workaround to add opacity to the background color
|
||||
function setOpacity(hex: string, alpha: number) {
|
||||
return `${hex}${Math.floor(alpha * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function Badge({ icon, value, color }: Props) {
|
||||
return (
|
||||
<span
|
||||
className="badge inline-flex items-center"
|
||||
style={{
|
||||
backgroundColor: setOpacity(color, 0.1),
|
||||
color,
|
||||
padding: '5px 10px',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { confirmDestructive } from '@/portainer/services/modal.service/confirm';
|
||||
import { Settings } from '@/react/portainer/settings/types';
|
||||
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
|
||||
import { PasswordLengthSlider } from './PasswordLengthSlider/PasswordLengthSlider';
|
||||
import { SaveAuthSettingsButton } from './SaveAuthSettingsButton';
|
||||
|
||||
export interface Props {
|
||||
onSaveSettings(): void;
|
||||
isLoading: boolean;
|
||||
value: Settings['InternalAuthSettings'];
|
||||
onChange(value: number): void;
|
||||
}
|
||||
|
||||
export function InternalAuth({
|
||||
onSaveSettings,
|
||||
isLoading,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) {
|
||||
function onSubmit() {
|
||||
if (value.RequiredPasswordLength < 10) {
|
||||
confirmDestructive({
|
||||
title: 'Allow weak passwords?',
|
||||
message:
|
||||
'You have set an insecure minimum password length. This could leave your system vulnerable to attack, are you sure?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Yes',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if (confirmed) onSaveSettings();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
onSaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSectionTitle>Information</FormSectionTitle>
|
||||
<div className="form-group col-sm-12 text-muted small">
|
||||
When using internal authentication, Portainer will encrypt user
|
||||
passwords and store credentials locally.
|
||||
</div>
|
||||
|
||||
<FormSectionTitle>Password rules</FormSectionTitle>
|
||||
<div className="form-group col-sm-12 text-muted small">
|
||||
Define minimum length for user-generated passwords.
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<PasswordLengthSlider
|
||||
min={1}
|
||||
max={18}
|
||||
step={1}
|
||||
value={value.RequiredPasswordLength}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SaveAuthSettingsButton onSubmit={onSubmit} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
.slider-badge {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.root {
|
||||
margin-left: 15px;
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.root :global(.rc-slider-tooltip-inner) {
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--bg-tooltip-color);
|
||||
min-width: max-content;
|
||||
color: var(--text-tooltip-color);
|
||||
box-shadow: var(--shadow-box-color);
|
||||
}
|
||||
|
||||
.root :global(.rc-slider-tooltip-arrow) {
|
||||
border-top-color: var(--bg-tooltip-color);
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import RcSlider from 'rc-slider';
|
||||
import clsx from 'clsx';
|
||||
import { Lock, XCircle, CheckCircle } from 'react-feather';
|
||||
|
||||
import 'rc-slider/assets/index.css';
|
||||
|
||||
import { Badge } from '../Badge';
|
||||
|
||||
import styles from './PasswordLengthSlider.module.css';
|
||||
|
||||
export interface Props {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
value: number;
|
||||
onChange(value: number): void;
|
||||
}
|
||||
|
||||
type Strength = 'weak' | 'good' | 'strong' | 'veryStrong';
|
||||
|
||||
const sliderProperties: Record<
|
||||
Strength,
|
||||
{ strength: string; color: string; text: string }
|
||||
> = {
|
||||
weak: {
|
||||
strength: 'weak',
|
||||
color: '#F04438',
|
||||
text: 'Weak password',
|
||||
},
|
||||
good: {
|
||||
strength: 'good',
|
||||
color: '#F79009',
|
||||
text: 'Good password',
|
||||
},
|
||||
strong: {
|
||||
strength: 'strong',
|
||||
color: '#12B76A',
|
||||
text: 'Strong password',
|
||||
},
|
||||
veryStrong: {
|
||||
strength: 'veryStrong',
|
||||
color: '#0BA5EC',
|
||||
text: 'Very strong password',
|
||||
},
|
||||
};
|
||||
|
||||
const SliderWithTooltip = RcSlider.createSliderWithTooltip(RcSlider);
|
||||
|
||||
export function PasswordLengthSlider({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) {
|
||||
const sliderProps = getSliderProps(value);
|
||||
|
||||
function getSliderProps(value: number) {
|
||||
if (value < 10) {
|
||||
return sliderProperties.weak;
|
||||
}
|
||||
|
||||
if (value < 12) {
|
||||
return sliderProperties.good;
|
||||
}
|
||||
|
||||
if (value < 14) {
|
||||
return sliderProperties.strong;
|
||||
}
|
||||
|
||||
return sliderProperties.veryStrong;
|
||||
}
|
||||
|
||||
function getBadgeIcon(strength: string) {
|
||||
switch (strength) {
|
||||
case 'weak':
|
||||
return <XCircle size="13" className="space-right" strokeWidth="3px" />;
|
||||
case 'good':
|
||||
case 'strong':
|
||||
return (
|
||||
<CheckCircle size="13" className="space-right" strokeWidth="3px" />
|
||||
);
|
||||
default:
|
||||
return <Lock size="13" className="space-right" strokeWidth="3px" />;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(sliderValue: number) {
|
||||
onChange(sliderValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.root, styles[sliderProps.strength])}>
|
||||
<div className="col-sm-4">
|
||||
<SliderWithTooltip
|
||||
tipFormatter={(value) => `${value} characters`}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
defaultValue={12}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
handleStyle={{
|
||||
height: 25,
|
||||
width: 25,
|
||||
borderWidth: 1.85,
|
||||
borderColor: sliderProps.color,
|
||||
top: 1.5,
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
railStyle={{ height: 10 }}
|
||||
trackStyle={{ backgroundColor: sliderProps.color, height: 10 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={clsx('col-sm-2', styles.sliderBadge)}>
|
||||
<Badge
|
||||
icon={getBadgeIcon(sliderProps.strength)}
|
||||
value={sliderProps.text}
|
||||
color={sliderProps.color}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
|
||||
export interface Props {
|
||||
onSubmit(): void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function SaveAuthSettingsButton({ onSubmit, isLoading }: Props) {
|
||||
return (
|
||||
<>
|
||||
<FormSectionTitle>Actions</FormSectionTitle>
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
loadingText="Saving..."
|
||||
isLoading={isLoading}
|
||||
onClick={() => onSubmit()}
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { InternalAuth } from './InternalAuth';
|
|
@ -0,0 +1,125 @@
|
|||
import { Field, Form, Formik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { useUpdateSettingsMutation } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
import { Settings } from '../types';
|
||||
|
||||
import { EnabledWaitingRoomSwitch } from './EnableWaitingRoomSwitch';
|
||||
|
||||
interface FormValues {
|
||||
EdgePortainerUrl: string;
|
||||
TrustOnFirstConnect: boolean;
|
||||
}
|
||||
const validation = yup.object({
|
||||
TrustOnFirstConnect: yup.boolean(),
|
||||
EdgePortainerUrl: yup
|
||||
.string()
|
||||
.test(
|
||||
'url',
|
||||
'URL should be a valid URI and cannot include localhost',
|
||||
(value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return !!url.hostname && url.hostname !== 'localhost';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
)
|
||||
.required('URL is required'),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const defaultUrl = buildDefaultUrl();
|
||||
|
||||
export function AutoEnvCreationSettingsForm({ settings }: Props) {
|
||||
const url = settings.EdgePortainerUrl;
|
||||
|
||||
const initialValues = {
|
||||
EdgePortainerUrl: url || defaultUrl,
|
||||
TrustOnFirstConnect: settings.TrustOnFirstConnect,
|
||||
};
|
||||
|
||||
const mutation = useUpdateSettingsMutation();
|
||||
|
||||
const { mutate: updateSettings } = mutation;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(variables: Partial<FormValues>) => {
|
||||
updateSettings(variables, {
|
||||
onSuccess() {
|
||||
notifySuccess(
|
||||
'Success',
|
||||
'Successfully updated Automatic Environment Creation settings'
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url && validation.isValidSync({ EdgePortainerUrl: defaultUrl })) {
|
||||
updateSettings({ EdgePortainerUrl: defaultUrl });
|
||||
}
|
||||
}, [updateSettings, url]);
|
||||
|
||||
return (
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
enableReinitialize
|
||||
>
|
||||
{({ errors, isValid, dirty }) => (
|
||||
<Form className="form-horizontal">
|
||||
<FormSectionTitle>Configuration</FormSectionTitle>
|
||||
|
||||
<FormControl
|
||||
label="Portainer URL"
|
||||
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
|
||||
inputId="url-input"
|
||||
errors={errors.EdgePortainerUrl}
|
||||
>
|
||||
<Field as={Input} id="url-input" name="EdgePortainerUrl" />
|
||||
</FormControl>
|
||||
|
||||
<EnabledWaitingRoomSwitch />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
loadingText="generating..."
|
||||
isLoading={mutation.isLoading}
|
||||
disabled={!isValid || !dirty}
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
function buildDefaultUrl() {
|
||||
const baseHREF = baseHref();
|
||||
return window.location.origin + (baseHREF !== '/' ? baseHREF : '');
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { useMutation } from 'react-query';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { generateKey } from '@/portainer/environments/environment.service/edge';
|
||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
|
||||
import { useSettings } from '../../queries';
|
||||
|
||||
import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm';
|
||||
|
||||
const commands = {
|
||||
linux: [
|
||||
commandsTabs.k8sLinux,
|
||||
commandsTabs.swarmLinux,
|
||||
commandsTabs.standaloneLinux,
|
||||
commandsTabs.nomadLinux,
|
||||
],
|
||||
win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow],
|
||||
};
|
||||
|
||||
export function AutomaticEdgeEnvCreation() {
|
||||
const edgeKeyMutation = useGenerateKeyMutation();
|
||||
const { mutate: generateKey } = edgeKeyMutation;
|
||||
const settingsQuery = useSettings();
|
||||
|
||||
const url = settingsQuery.data?.EdgePortainerUrl;
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
generateKey();
|
||||
}
|
||||
}, [generateKey, url]);
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const edgeKey = edgeKeyMutation.data;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetTitle
|
||||
icon="svg-laptop"
|
||||
title="Automatic Edge Environment Creation"
|
||||
/>
|
||||
<WidgetBody>
|
||||
<AutoEnvCreationSettingsForm settings={settingsQuery.data} />
|
||||
|
||||
{edgeKeyMutation.isLoading ? (
|
||||
<div>Generating key for {url} ... </div>
|
||||
) : (
|
||||
edgeKey && (
|
||||
<EdgeScriptForm
|
||||
edgeInfo={{ key: edgeKey }}
|
||||
commands={commands}
|
||||
isNomadTokenVisible
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// using mutation because we want this action to run only when required
|
||||
function useGenerateKeyMutation() {
|
||||
return useMutation(generateKey);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
|
||||
export function EnabledWaitingRoomSwitch() {
|
||||
const [inputProps, meta, helpers] = useField<boolean>('TrustOnFirstConnect');
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
inputId="edge_waiting_room"
|
||||
label="Disable Edge Environment Waiting Room"
|
||||
errors={meta.error}
|
||||
>
|
||||
<Switch
|
||||
id="edge_waiting_room"
|
||||
name="TrustOnFirstConnect"
|
||||
className="space-right"
|
||||
checked={inputProps.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
async function handleChange(trust: boolean) {
|
||||
if (!trust) {
|
||||
helpers.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Disable Edge Environment Waiting Room',
|
||||
message:
|
||||
'By disabling the waiting room feature, all devices requesting association will be automatically associated and could pose a security risk. Are you sure?',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
helpers.setValue(!!confirmed);
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
|
@ -0,0 +1,142 @@
|
|||
import { Form, Formik } from 'formik';
|
||||
import { useReducer } from 'react';
|
||||
|
||||
import { EdgeCheckinIntervalField } from '@/edge/components/EdgeCheckInIntervalField';
|
||||
import { EdgeAsyncIntervalsForm } from '@/edge/components/EdgeAsyncIntervalsForm';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
|
||||
import { useSettings, useUpdateSettingsMutation } from '../../queries';
|
||||
|
||||
import { FormValues } from './types';
|
||||
|
||||
const asyncIntervalFieldSettings = {
|
||||
ping: {
|
||||
label: 'Edge agent default ping frequency',
|
||||
tooltip:
|
||||
'Interval used by default by each Edge agent to ping the Portainer instance. Affects Edge environment management and Edge compute features.',
|
||||
},
|
||||
snapshot: {
|
||||
label: 'Edge agent default snapshot frequency',
|
||||
tooltip:
|
||||
'Interval used by default by each Edge agent to snapshot the agent state.',
|
||||
},
|
||||
command: {
|
||||
label: 'Edge agent default command frequency',
|
||||
tooltip: 'Interval used by default by each Edge agent to execute commands.',
|
||||
},
|
||||
};
|
||||
|
||||
export function DeploymentSyncOptions() {
|
||||
const settingsQuery = useSettings();
|
||||
const settingsMutation = useUpdateSettingsMutation();
|
||||
const [formKey, resetForm] = useReducer((state) => state + 1, 0);
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
Edge: settingsQuery.data.Edge,
|
||||
EdgeAgentCheckinInterval: settingsQuery.data.EdgeAgentCheckinInterval,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="svg-laptop" title="Deployment sync options" />
|
||||
<WidgetBody>
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
key={formKey}
|
||||
>
|
||||
{({ errors, setFieldValue, values, isValid, dirty }) => (
|
||||
<Form className="form-horizontal">
|
||||
<FormControl
|
||||
inputId="edge_async_mode"
|
||||
label="Use Async mode by default"
|
||||
size="small"
|
||||
errors={errors?.Edge?.AsyncMode}
|
||||
tooltip="Using Async allows the ability to define different ping,
|
||||
snapshot and command frequencies."
|
||||
>
|
||||
<Switch
|
||||
id="edge_async_mode"
|
||||
name="edge_async_mode"
|
||||
className="space-right"
|
||||
checked={values.Edge.AsyncMode}
|
||||
onChange={(e) =>
|
||||
setFieldValue('Edge.AsyncMode', e.valueOf())
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="orange">
|
||||
Enabling Async disables the tunnel function.
|
||||
</TextTip>
|
||||
|
||||
<FormSection title="Check-in Intervals">
|
||||
{!values.Edge.AsyncMode ? (
|
||||
<EdgeCheckinIntervalField
|
||||
value={values.EdgeAgentCheckinInterval}
|
||||
onChange={(value) =>
|
||||
setFieldValue('EdgeAgentCheckinInterval', value)
|
||||
}
|
||||
isDefaultHidden
|
||||
label="Edge agent default poll frequency"
|
||||
tooltip="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
|
||||
/>
|
||||
) : (
|
||||
<EdgeAsyncIntervalsForm
|
||||
values={values.Edge}
|
||||
onChange={(value) => setFieldValue('Edge', value)}
|
||||
isDefaultHidden
|
||||
fieldSettings={asyncIntervalFieldSettings}
|
||||
/>
|
||||
)}
|
||||
</FormSection>
|
||||
|
||||
<FormSection title="Actions">
|
||||
<div className="form-group mt-5">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-deploySyncOptionsButton"
|
||||
isLoading={settingsMutation.isLoading}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormSection>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleSubmit(values: FormValues) {
|
||||
settingsMutation.mutate(
|
||||
{
|
||||
Edge: values.Edge,
|
||||
EdgeAgentCheckinInterval: values.EdgeAgentCheckinInterval,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess('Success', 'Settings updated successfully');
|
||||
resetForm();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface FormValues {
|
||||
Edge: {
|
||||
PingInterval: number;
|
||||
SnapshotInterval: number;
|
||||
CommandInterval: number;
|
||||
AsyncMode: boolean;
|
||||
};
|
||||
EdgeAgentCheckinInterval: number;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { boolean, number, object, SchemaOf } from 'yup';
|
||||
|
||||
import { options as asyncIntervalOptions } from '@/edge/components/EdgeAsyncIntervalsForm';
|
||||
|
||||
import { FormValues } from './types';
|
||||
|
||||
const intervals = asyncIntervalOptions.map((option) => option.value);
|
||||
|
||||
export function validationSchema(): SchemaOf<FormValues> {
|
||||
return object({
|
||||
EdgeAgentCheckinInterval: number().required('This field is required.'),
|
||||
Edge: object({
|
||||
PingInterval: number()
|
||||
.required('This field is required.')
|
||||
.oneOf(intervals),
|
||||
SnapshotInterval: number()
|
||||
.required('This field is required.')
|
||||
.oneOf(intervals),
|
||||
CommandInterval: number()
|
||||
.required('This field is required.')
|
||||
.oneOf(intervals),
|
||||
AsyncMode: boolean().default(false),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
import { Formik, Form } from 'formik';
|
||||
|
||||
import { EdgeCheckinIntervalField } from '@/edge/components/EdgeCheckInIntervalField';
|
||||
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
|
||||
import { Settings } from '../types';
|
||||
|
||||
import { validationSchema } from './EdgeComputeSettings.validation';
|
||||
|
||||
export interface FormValues {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings?: Settings;
|
||||
onSubmit(values: FormValues): void;
|
||||
}
|
||||
|
||||
export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="svg-laptop" title="Edge Compute settings" />
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={settings}
|
||||
enableReinitialize
|
||||
validationSchema={() => validationSchema()}
|
||||
onSubmit={onSubmit}
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
dirty,
|
||||
}) => (
|
||||
<Form
|
||||
className="form-horizontal"
|
||||
onSubmit={handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<FormControl
|
||||
inputId="edge_enable"
|
||||
label="Enable Edge Compute features"
|
||||
size="small"
|
||||
errors={errors.EnableEdgeComputeFeatures}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enable"
|
||||
name="edge_enable"
|
||||
className="space-right"
|
||||
checked={values.EnableEdgeComputeFeatures}
|
||||
onChange={(e) =>
|
||||
setFieldValue('EnableEdgeComputeFeatures', e)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
When enabled, this will enable Portainer to execute Edge
|
||||
Device features.
|
||||
</TextTip>
|
||||
|
||||
<FormControl
|
||||
inputId="edge_enforce_id"
|
||||
label="Enforce use of Portainer generated Edge ID"
|
||||
size="small"
|
||||
tooltip="This setting only applies to manually created environments."
|
||||
errors={errors.EnforceEdgeID}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enforce_id"
|
||||
name="edge_enforce_id"
|
||||
className="space-right"
|
||||
checked={values.EnforceEdgeID}
|
||||
onChange={(e) =>
|
||||
setFieldValue('EnforceEdgeID', e.valueOf())
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormSectionTitle>Check-in Intervals</FormSectionTitle>
|
||||
|
||||
<EdgeCheckinIntervalField
|
||||
value={values.EdgeAgentCheckinInterval}
|
||||
onChange={(value) =>
|
||||
setFieldValue('EdgeAgentCheckinInterval', value)
|
||||
}
|
||||
isDefaultHidden
|
||||
label="Edge agent default poll frequency"
|
||||
tooltip="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features."
|
||||
/>
|
||||
|
||||
<div className="form-group mt-5">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-edgeComputeButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { boolean, number, object } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
EdgeAgentCheckinInterval: number().required('This field is required.'),
|
||||
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
||||
EnforceEdgeID: boolean().required('This field is required.'),
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { EdgeComputeSettings } from './EdgeComputeSettings';
|
|
@ -0,0 +1,7 @@
|
|||
export interface Settings {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { Settings } from '@/react/portainer/settings/types';
|
||||
|
||||
import { EdgeComputeSettings } from './EdgeComputeSettings';
|
||||
import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation';
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
onSubmit(values: Settings): void;
|
||||
}
|
||||
|
||||
export function EdgeComputeSettingsView({ settings, onSubmit }: Props) {
|
||||
return (
|
||||
<div className="row">
|
||||
<EdgeComputeSettings settings={settings} onSubmit={onSubmit} />
|
||||
|
||||
{process.env.PORTAINER_EDITION === 'BE' && <AutomaticEdgeEnvCreation />}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
import { useTable, usePagination, useSortBy } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import PortainerError from '@/portainer/error';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@@/datatables';
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
import { useColumns } from './columns';
|
||||
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
|
||||
|
||||
export interface FDOProfilesTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
export interface FDOProfilesDatatableProps {
|
||||
isFDOEnabled: boolean;
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<FDOProfilesTableSettings>();
|
||||
const columns = useColumns();
|
||||
|
||||
const { isLoading, profiles, error } = useFDOProfiles();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Profile>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: profiles,
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return isFDOEnabled;
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="list" featherIcon label="Device Profiles">
|
||||
<FDOProfilesDatatableActions
|
||||
isFDOEnabled={isFDOEnabled}
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
/>
|
||||
</TableTitle>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Profile>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{!isLoading && profiles && profiles.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<TableRow<Profile>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
{userMessage(isLoading, error)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={profiles ? profiles.length : 0}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
}
|
||||
|
||||
function userMessage(isLoading: boolean, error?: PortainerError) {
|
||||
if (isLoading) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return 'No profiles found';
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import { useQueryClient } from 'react-query';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import {
|
||||
confirmAsync,
|
||||
confirmDestructiveAsync,
|
||||
} from '@/portainer/services/modal.service/confirm';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import {
|
||||
deleteProfile,
|
||||
duplicateProfile,
|
||||
} from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
interface Props {
|
||||
isFDOEnabled: boolean;
|
||||
selectedItems: Profile[];
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatableActions({
|
||||
isFDOEnabled,
|
||||
selectedItems,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return (
|
||||
<div className="actionBar">
|
||||
<Link to="portainer.endpoints.profile" className="space-left">
|
||||
<Button disabled={!isFDOEnabled} icon="plus-circle" featherIcon>
|
||||
Add Profile
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
disabled={!isFDOEnabled || selectedItems.length !== 1}
|
||||
onClick={() => onDuplicateProfileClick()}
|
||||
icon="plus-circle"
|
||||
featherIcon
|
||||
>
|
||||
Duplicate
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={!isFDOEnabled || selectedItems.length < 1}
|
||||
color="danger"
|
||||
onClick={() => onDeleteProfileClick()}
|
||||
icon="trash-2"
|
||||
featherIcon
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function onDuplicateProfileClick() {
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will duplicate the selected profile. Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = selectedItems[0];
|
||||
const newProfile = await duplicateProfile(profile.id);
|
||||
notifications.success('Profile successfully duplicated', profile.name);
|
||||
router.stateService.go('portainer.endpoints.profile.edit', {
|
||||
id: newProfile.id,
|
||||
});
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to duplicate profile'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteProfileClick() {
|
||||
const confirmed = await confirmDestructiveAsync({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will delete the selected profile(s). Continue?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
selectedItems.map(async (profile) => {
|
||||
try {
|
||||
await deleteProfile(profile.id);
|
||||
|
||||
notifications.success('Profile successfully removed', profile.name);
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to remove profile'
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await queryClient.invalidateQueries('fdo_profiles');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import {
|
||||
FDOProfilesDatatable,
|
||||
FDOProfilesDatatableProps,
|
||||
} from './FDOProfilesDatatable';
|
||||
|
||||
export function FDOProfilesDatatableContainer({
|
||||
...props
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const defaultSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey="fdoProfiles">
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FDOProfilesDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const created: Column<Profile> = {
|
||||
Header: 'Created',
|
||||
accessor: 'dateCreated',
|
||||
id: 'created',
|
||||
Cell: ({ value }) => isoDateFromTimestamp(value),
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
Filter: () => null,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { created } from './created';
|
||||
import { name } from './name';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, created], []);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { useSref } from '@uirouter/react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const name: Column<Profile> = {
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: true,
|
||||
sortType: 'string',
|
||||
};
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
row: { original: profile },
|
||||
}: CellProps<Profile>) {
|
||||
const linkProps = useSref('portainer.endpoints.profile.edit', {
|
||||
id: profile.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import PortainerError from '@/portainer/error';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { getProfiles } from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
export function useFDOProfiles() {
|
||||
const { isLoading, data, isError, error } = useQuery('fdo_profiles', () =>
|
||||
getProfiles()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
error as Error,
|
||||
'Failed retrieving FDO profiles'
|
||||
);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
const profiles = useMemo(() => data || [], [data]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
profiles,
|
||||
error: isError ? (error as PortainerError) : undefined,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.fdo-table {
|
||||
margin-top: 3em;
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
|
||||
import { FDOConfiguration } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
import { FDOProfilesDatatableContainer } from '../FDOProfilesDatatable/FDOProfilesDatatableContainer';
|
||||
|
||||
import styles from './SettingsFDO.module.css';
|
||||
import { validationSchema } from './SettingsFDO.validation';
|
||||
|
||||
export interface Settings {
|
||||
fdoConfiguration: FDOConfiguration;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
onSubmit(values: FDOConfiguration): void;
|
||||
}
|
||||
|
||||
export function SettingsFDO({ settings, onSubmit }: Props) {
|
||||
const fdoConfiguration = settings ? settings.fdoConfiguration : null;
|
||||
const initialFDOEnabled = fdoConfiguration ? fdoConfiguration.enabled : false;
|
||||
|
||||
const [isFDOEnabled, setIsFDOEnabled] = useState(initialFDOEnabled);
|
||||
useEffect(() => {
|
||||
setIsFDOEnabled(settings?.fdoConfiguration?.enabled);
|
||||
}, [settings]);
|
||||
|
||||
const initialValues = {
|
||||
enabled: initialFDOEnabled,
|
||||
ownerURL: fdoConfiguration ? fdoConfiguration.ownerURL : '',
|
||||
ownerUsername: fdoConfiguration ? fdoConfiguration.ownerUsername : '',
|
||||
ownerPassword: fdoConfiguration ? fdoConfiguration.ownerPassword : '',
|
||||
};
|
||||
|
||||
const edgeComputeFeaturesEnabled = settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="svg-laptop" title="FDO" />
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
enableReinitialize
|
||||
validationSchema={() => validationSchema()}
|
||||
validateOnChange
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
dirty,
|
||||
}) => (
|
||||
<Form className="form-horizontal" onSubmit={handleSubmit}>
|
||||
<FormControl
|
||||
inputId="edge_enableFDO"
|
||||
label="Enable FDO Management Service"
|
||||
size="small"
|
||||
errors={errors.enabled}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enableFDO"
|
||||
name="edge_enableFDO"
|
||||
className="space-right"
|
||||
disabled={!edgeComputeFeaturesEnabled}
|
||||
checked={edgeComputeFeaturesEnabled && values.enabled}
|
||||
onChange={(e) => onChangedEnabled(e, setFieldValue)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
When enabled, this will allow Portainer to interact with FDO
|
||||
Services.
|
||||
</TextTip>
|
||||
|
||||
{edgeComputeFeaturesEnabled && values.enabled && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="owner_url"
|
||||
label="Owner Service Server"
|
||||
errors={errors.ownerURL}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="ownerURL"
|
||||
id="owner_url"
|
||||
placeholder="http://127.0.0.1:8042"
|
||||
value={values.ownerURL}
|
||||
data-cy="fdo-serverInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="owner_username"
|
||||
label="Owner Service Username"
|
||||
errors={errors.ownerUsername}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="ownerUsername"
|
||||
id="owner_username"
|
||||
placeholder="username"
|
||||
value={values.ownerUsername}
|
||||
data-cy="fdo-usernameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="owner_password"
|
||||
label="Owner Service Password"
|
||||
errors={errors.ownerPassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="ownerPassword"
|
||||
id="owner_password"
|
||||
placeholder="password"
|
||||
value={values.ownerPassword}
|
||||
data-cy="fdo-passwordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group mt-5">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-fdoButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
{edgeComputeFeaturesEnabled && isFDOEnabled && (
|
||||
<div className={styles.fdoTable}>
|
||||
<FormSectionTitle>Device Profiles</FormSectionTitle>
|
||||
<TextTip color="blue">
|
||||
Add, Edit and Manage the list of device profiles available
|
||||
during FDO device setup
|
||||
</TextTip>
|
||||
<FDOProfilesDatatableContainer isFDOEnabled={initialFDOEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function onChangedEnabled(
|
||||
e: boolean,
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: unknown,
|
||||
shouldValidate?: boolean
|
||||
) => void
|
||||
) {
|
||||
setIsFDOEnabled(e);
|
||||
setFieldValue('enabled', e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
ownerURL: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
ownerUsername: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
ownerPassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { SettingsFDO } from './SettingsFDO';
|
|
@ -0,0 +1,267 @@
|
|||
import { useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
|
||||
import { OpenAMTConfiguration } from '@/portainer/hostmanagement/open-amt/model';
|
||||
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { FileUploadField } from '@@/form-components/FileUpload';
|
||||
|
||||
import { validationSchema } from './SettingsOpenAMT.validation';
|
||||
|
||||
export interface Settings {
|
||||
openAMTConfiguration: OpenAMTConfiguration;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
onSubmit(values: OpenAMTConfiguration): void;
|
||||
}
|
||||
|
||||
export function SettingsOpenAMT({ settings, onSubmit }: Props) {
|
||||
const [certFile, setCertFile] = useState<File>();
|
||||
async function handleFileUpload(
|
||||
file: File,
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: unknown,
|
||||
shouldValidate?: boolean
|
||||
) => void
|
||||
) {
|
||||
if (file) {
|
||||
setCertFile(file);
|
||||
const fileContent = await readFileContent(file);
|
||||
setFieldValue('certFileContent', fileContent);
|
||||
setFieldValue('certFileName', file.name);
|
||||
}
|
||||
}
|
||||
|
||||
function readFileContent(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
if (e.target == null || e.target.result == null) {
|
||||
resolve('');
|
||||
return;
|
||||
}
|
||||
const base64 = e.target.result.toString();
|
||||
// remove prefix of "data:application/x-pkcs12;base64," returned by "readAsDataURL()"
|
||||
const index = base64.indexOf('base64,');
|
||||
const cert = base64.substring(index + 7, base64.length);
|
||||
resolve(cert);
|
||||
};
|
||||
fileReader.onerror = () => {
|
||||
reject(new Error('error reading provisioning certificate file'));
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const openAMTConfiguration = settings ? settings.openAMTConfiguration : null;
|
||||
const initialValues = {
|
||||
enabled: openAMTConfiguration ? openAMTConfiguration.enabled : false,
|
||||
mpsServer: openAMTConfiguration ? openAMTConfiguration.mpsServer : '',
|
||||
mpsUser: openAMTConfiguration ? openAMTConfiguration.mpsUser : '',
|
||||
mpsPassword: openAMTConfiguration ? openAMTConfiguration.mpsPassword : '',
|
||||
domainName: openAMTConfiguration ? openAMTConfiguration.domainName : '',
|
||||
certFileContent: openAMTConfiguration
|
||||
? openAMTConfiguration.certFileContent
|
||||
: '',
|
||||
certFileName: openAMTConfiguration ? openAMTConfiguration.certFileName : '',
|
||||
certFilePassword: openAMTConfiguration
|
||||
? openAMTConfiguration.certFilePassword
|
||||
: '',
|
||||
};
|
||||
|
||||
if (
|
||||
initialValues.certFileContent &&
|
||||
initialValues.certFileName &&
|
||||
!certFile
|
||||
) {
|
||||
setCertFile(new File([], initialValues.certFileName));
|
||||
}
|
||||
|
||||
const edgeComputeFeaturesEnabled = settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon="svg-laptop" title="Intel OpenAMT" />
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
enableReinitialize
|
||||
validationSchema={() => validationSchema()}
|
||||
validateOnChange
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
dirty,
|
||||
}) => (
|
||||
<Form className="form-horizontal" onSubmit={handleSubmit}>
|
||||
<FormControl
|
||||
inputId="edge_enableOpenAMT"
|
||||
label="Enable OpenAMT"
|
||||
errors={errors.enabled}
|
||||
size="small"
|
||||
>
|
||||
<Switch
|
||||
id="edge_enableOpenAMT"
|
||||
name="edge_enableOpenAMT"
|
||||
className="space-right"
|
||||
disabled={!edgeComputeFeaturesEnabled}
|
||||
checked={edgeComputeFeaturesEnabled && values.enabled}
|
||||
onChange={(e) => setFieldValue('enabled', e)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue">
|
||||
When enabled, this will allow Portainer to interact with an
|
||||
OpenAMT MPS API.
|
||||
</TextTip>
|
||||
|
||||
{edgeComputeFeaturesEnabled && values.enabled && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="mps_server"
|
||||
label="MPS Server"
|
||||
size="medium"
|
||||
errors={errors.mpsServer}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="mpsServer"
|
||||
id="mps_server"
|
||||
placeholder="Enter the MPS Server"
|
||||
value={values.mpsServer}
|
||||
data-cy="openAMT-serverInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="mps_username"
|
||||
label="MPS User"
|
||||
size="medium"
|
||||
errors={errors.mpsUser}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="mpsUser"
|
||||
id="mps_username"
|
||||
placeholder="Enter the MPS User"
|
||||
value={values.mpsUser}
|
||||
data-cy="openAMT-usernameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="mps_password"
|
||||
label="MPS Password"
|
||||
size="medium"
|
||||
tooltip="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
errors={errors.mpsPassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="mpsPassword"
|
||||
id="mps_password"
|
||||
placeholder="Enter the MPS Password"
|
||||
value={values.mpsPassword}
|
||||
data-cy="openAMT-passwordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="domain_name"
|
||||
label="Domain Name"
|
||||
size="medium"
|
||||
tooltip="Enter the FQDN that is associated with the provisioning certificate (i.e amtdomain.com)"
|
||||
errors={errors.domainName}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="domainName"
|
||||
id="domain_name"
|
||||
placeholder="Enter the Domain Name"
|
||||
value={values.domainName}
|
||||
data-cy="openAMT-domainInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="certificate_file"
|
||||
label="Provisioning Certificate File (.pfx)"
|
||||
size="medium"
|
||||
tooltip="Supported CAs are Comodo, DigiCert, Entrust and GoDaddy.<br>The certificate must contain the private key.<br>On AMT 15 based devices you need to use SHA2."
|
||||
errors={errors.certFileContent}
|
||||
>
|
||||
<FileUploadField
|
||||
inputId="certificate_file"
|
||||
title="Upload file"
|
||||
accept=".pfx"
|
||||
value={certFile}
|
||||
onChange={(file) =>
|
||||
handleFileUpload(file, setFieldValue)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="certificate_password"
|
||||
label="Provisioning Certificate Password"
|
||||
size="medium"
|
||||
tooltip="Needs to be 8-32 characters including one uppercase, one lowercase letters, one base-10 digit and one special character."
|
||||
errors={errors.certFilePassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="certFilePassword"
|
||||
id="certificate_password"
|
||||
placeholder="**********"
|
||||
value={values.certFilePassword}
|
||||
data-cy="openAMT-certPasswordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group mt-5">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-fdoButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
mpsServer: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
mpsUser: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
mpsPassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
domainName: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFileContent: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFileName: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
certFilePassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { SettingsOpenAMT } from './SettingsOpenAMT';
|
7
app/react/portainer/settings/EdgeComputeView/types.ts
Normal file
7
app/react/portainer/settings/EdgeComputeView/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface Settings {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
68
app/react/portainer/settings/queries.ts
Normal file
68
app/react/portainer/settings/queries.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
|
||||
import {
|
||||
mutationOptions,
|
||||
withError,
|
||||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
|
||||
import {
|
||||
getSettings,
|
||||
updateSettings,
|
||||
getPublicSettings,
|
||||
updateDefaultRegistry,
|
||||
} from './settings.service';
|
||||
import { DefaultRegistry, Settings } from './types';
|
||||
|
||||
export function usePublicSettings<T = PublicSettingsViewModel>({
|
||||
enabled,
|
||||
select,
|
||||
onSuccess,
|
||||
}: {
|
||||
select?: (settings: PublicSettingsViewModel) => T;
|
||||
enabled?: boolean;
|
||||
onSuccess?: (data: T) => void;
|
||||
} = {}) {
|
||||
return useQuery(['settings', 'public'], () => getPublicSettings(), {
|
||||
select,
|
||||
...withError('Unable to retrieve public settings'),
|
||||
enabled,
|
||||
onSuccess,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSettings<T = Settings>(
|
||||
select?: (settings: Settings) => T,
|
||||
enabled?: boolean
|
||||
) {
|
||||
return useQuery(['settings'], getSettings, {
|
||||
select,
|
||||
enabled,
|
||||
...withError('Unable to retrieve settings'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSettingsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
updateSettings,
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [['settings'], ['cloud']]),
|
||||
withError('Unable to update settings')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateDefaultRegistrySettingsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(payload: Partial<DefaultRegistry>) => updateDefaultRegistry(payload),
|
||||
mutationOptions(
|
||||
withInvalidate(queryClient, [['settings']]),
|
||||
withError('Unable to update default registry settings')
|
||||
)
|
||||
);
|
||||
}
|
64
app/react/portainer/settings/settings.service.ts
Normal file
64
app/react/portainer/settings/settings.service.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { DefaultRegistry, PublicSettingsResponse, Settings } from './types';
|
||||
|
||||
export async function getPublicSettings() {
|
||||
try {
|
||||
const { data } = await axios.get<PublicSettingsResponse>(
|
||||
buildUrl('public')
|
||||
);
|
||||
return new PublicSettingsViewModel(data);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to retrieve application settings'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
try {
|
||||
const { data } = await axios.get<Settings>(buildUrl());
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to retrieve application settings'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSettings(settings: Partial<Settings>) {
|
||||
try {
|
||||
await axios.put(buildUrl(), settings);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update application settings');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDefaultRegistry(
|
||||
defaultRegistry: Partial<DefaultRegistry>
|
||||
) {
|
||||
try {
|
||||
await axios.put(buildUrl('default_registry'), defaultRegistry);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
e as Error,
|
||||
'Unable to update default registry settings'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUrl(subResource?: string, action?: string) {
|
||||
let url = 'settings';
|
||||
if (subResource) {
|
||||
url += `/${subResource}`;
|
||||
}
|
||||
|
||||
if (action) {
|
||||
url += `/${action}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
159
app/react/portainer/settings/types.ts
Normal file
159
app/react/portainer/settings/types.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
export interface FDOConfiguration {
|
||||
enabled: boolean;
|
||||
ownerURL: string;
|
||||
ownerUsername: string;
|
||||
ownerPassword: string;
|
||||
}
|
||||
|
||||
export interface TLSConfiguration {
|
||||
TLS: boolean;
|
||||
TLSSkipVerify: boolean;
|
||||
TLSCACert?: string;
|
||||
TLSCert?: string;
|
||||
TLSKey?: string;
|
||||
}
|
||||
|
||||
export interface LDAPGroupSearchSettings {
|
||||
GroupBaseDN: string;
|
||||
GroupFilter: string;
|
||||
GroupAttribute: string;
|
||||
}
|
||||
|
||||
export interface LDAPSearchSettings {
|
||||
BaseDN: string;
|
||||
Filter: string;
|
||||
UserNameAttribute: string;
|
||||
}
|
||||
|
||||
export interface LDAPSettings {
|
||||
AnonymousMode: boolean;
|
||||
ReaderDN: string;
|
||||
Password?: string;
|
||||
URL: string;
|
||||
TLSConfig: TLSConfiguration;
|
||||
StartTLS: boolean;
|
||||
SearchSettings: LDAPSearchSettings[];
|
||||
GroupSearchSettings: LDAPGroupSearchSettings[];
|
||||
AutoCreateUsers: boolean;
|
||||
}
|
||||
|
||||
export interface Pair {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface OpenAMTConfiguration {
|
||||
enabled: boolean;
|
||||
mpsServer: string;
|
||||
mpsUser: string;
|
||||
mpsPassword: string;
|
||||
mpsToken: string;
|
||||
certFileName: string;
|
||||
certFileContent: string;
|
||||
certFilePassword: string;
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
export interface OAuthSettings {
|
||||
ClientID: string;
|
||||
ClientSecret?: string;
|
||||
AccessTokenURI: string;
|
||||
AuthorizationURI: string;
|
||||
ResourceURI: string;
|
||||
RedirectURI: string;
|
||||
UserIdentifier: string;
|
||||
Scopes: string;
|
||||
OAuthAutoCreateUsers: boolean;
|
||||
DefaultTeamID: TeamId;
|
||||
SSO: boolean;
|
||||
LogoutURI: string;
|
||||
KubeSecretKey: string;
|
||||
}
|
||||
|
||||
enum AuthenticationMethod {
|
||||
/**
|
||||
* Internal represents the internal authentication method (authentication against Portainer API)
|
||||
*/
|
||||
Internal,
|
||||
/**
|
||||
* LDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||
*/
|
||||
LDAP,
|
||||
/**
|
||||
* OAuth represents the OAuth authentication method (authentication against a authorization server)
|
||||
*/
|
||||
OAuth,
|
||||
}
|
||||
|
||||
type Feature = string;
|
||||
|
||||
export interface DefaultRegistry {
|
||||
Hide: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
LogoURL: string;
|
||||
BlackListedLabels: Pair[];
|
||||
AuthenticationMethod: AuthenticationMethod;
|
||||
InternalAuthSettings: { RequiredPasswordLength: number };
|
||||
LDAPSettings: LDAPSettings;
|
||||
OAuthSettings: OAuthSettings;
|
||||
openAMTConfiguration: OpenAMTConfiguration;
|
||||
fdoConfiguration: FDOConfiguration;
|
||||
FeatureFlagSettings: { [key: Feature]: boolean };
|
||||
SnapshotInterval: string;
|
||||
TemplatesURL: string;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
UserSessionTimeout: string;
|
||||
KubeconfigExpiry: string;
|
||||
EnableTelemetry: boolean;
|
||||
HelmRepositoryURL: string;
|
||||
KubectlShellImage: string;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
AgentSecret: string;
|
||||
EdgePortainerUrl: string;
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EdgeCommandInterval: number;
|
||||
EdgePingInterval: number;
|
||||
EdgeSnapshotInterval: number;
|
||||
DisplayDonationHeader: boolean;
|
||||
DisplayExternalContributors: boolean;
|
||||
EnableHostManagementFeatures: boolean;
|
||||
AllowVolumeBrowserForRegularUsers: boolean;
|
||||
AllowBindMountsForRegularUsers: boolean;
|
||||
AllowPrivilegedModeForRegularUsers: boolean;
|
||||
AllowHostNamespaceForRegularUsers: boolean;
|
||||
AllowStackManagementForRegularUsers: boolean;
|
||||
AllowDeviceMappingForRegularUsers: boolean;
|
||||
AllowContainerCapabilitiesForRegularUsers: boolean;
|
||||
Edge: {
|
||||
PingInterval: number;
|
||||
SnapshotInterval: number;
|
||||
CommandInterval: number;
|
||||
AsyncMode: 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;
|
||||
// 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;
|
||||
}
|
|
@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from 'react-query';
|
|||
import { Trash2, Users } from 'react-feather';
|
||||
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import {
|
||||
mutationOptions,
|
||||
withError,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useRouter } from '@uirouter/react';
|
|||
|
||||
import { useUsers } from '@/portainer/users/queries';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
FileText,
|
||||
} from 'react-feather';
|
||||
|
||||
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
import {
|
||||
FeatureFlag,
|
||||
useFeatureFlag,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Home } from 'react-feather';
|
|||
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { useIsTeamLeader } from '@/portainer/users/queries';
|
||||
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||
|
||||
import styles from './Sidebar.module.css';
|
||||
import { EdgeComputeSidebar } from './EdgeComputeSidebar';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue