diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js
index 04ea356f9..82b8b9324 100644
--- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js
+++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js
@@ -206,6 +206,7 @@ class CreateCustomTemplateViewController {
this.state.endpointMode = applicationState.endpoint.mode;
let stackType = 0;
if (this.state.endpointMode.provider === 'DOCKER_STANDALONE') {
+ this.isDockerStandalone = true;
stackType = 2;
} else if (this.state.endpointMode.provider === 'DOCKER_SWARM_MODE') {
stackType = 1;
diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js
index a3e011741..4107a3947 100644
--- a/app/portainer/views/stacks/create/createStackController.js
+++ b/app/portainer/views/stacks/create/createStackController.js
@@ -1,5 +1,4 @@
import angular from 'angular';
-import uuidv4 from 'uuid/v4';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
@@ -9,6 +8,8 @@ import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { editor, upload, git, customTemplate } from '@@/BoxSelector/common-options/build-methods';
import { confirmWebEditorDiscard } from '@@/modals/confirm';
+import { parseAutoUpdateResponse, transformAutoUpdateViewModel } from '@/react/portainer/gitops/AutoUpdateFieldset/utils';
+import { baseStackWebhookUrl } from '@/portainer/helpers/webhookHelper';
angular
.module('portainer.app')
@@ -29,8 +30,6 @@ angular
ContainerHelper,
CustomTemplateService,
ContainerService,
- WebhookHelper,
- clipboard,
endpoint
) {
$scope.onChangeTemplateId = onChangeTemplateId;
@@ -55,11 +54,9 @@ angular
AdditionalFiles: [],
ComposeFilePathInRepository: 'docker-compose.yml',
AccessControlData: new AccessControlFormData(),
- RepositoryAutomaticUpdates: false,
- RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
- RepositoryFetchInterval: '5m',
- RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
+ EnableWebhook: false,
Variables: {},
+ AutoUpdate: parseAutoUpdateResponse(),
};
$scope.state = {
@@ -72,6 +69,7 @@ angular
isEditorDirty: false,
selectedTemplate: null,
selectedTemplateId: null,
+ baseWebhookUrl: baseStackWebhookUrl(),
};
$window.onbeforeunload = () => {
@@ -99,14 +97,6 @@ angular
});
};
- $scope.addAdditionalFiles = function () {
- $scope.formValues.AdditionalFiles.push('');
- };
-
- $scope.removeAdditionalFiles = function (index) {
- $scope.formValues.AdditionalFiles.splice(index, 1);
- };
-
function buildAnalyticsProperties() {
const metadata = { type: methodLabel($scope.state.Method) };
@@ -163,7 +153,6 @@ angular
function createSwarmStack(name, method) {
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
const endpointId = +$state.params.endpointId;
-
if (method === 'template' || method === 'editor') {
var stackFileContent = $scope.formValues.StackFileContent;
return StackService.createSwarmStackFromFileContent(name, stackFileContent, env, endpointId);
@@ -183,25 +172,13 @@ angular
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword,
+ AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
};
- getAutoUpdatesProperty(repositoryOptions);
-
return StackService.createSwarmStackFromGitRepository(name, repositoryOptions, env, endpointId);
}
}
- function getAutoUpdatesProperty(repositoryOptions) {
- if ($scope.formValues.RepositoryAutomaticUpdates) {
- repositoryOptions.AutoUpdate = {};
- if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.INTERVAL) {
- repositoryOptions.AutoUpdate.Interval = $scope.formValues.RepositoryFetchInterval;
- } else if ($scope.formValues.RepositoryMechanism === RepositoryMechanismTypes.WEBHOOK) {
- repositoryOptions.AutoUpdate.Webhook = $scope.formValues.RepositoryWebhookURL.split('/').reverse()[0];
- }
- }
- }
-
function createComposeStack(name, method) {
var env = FormHelper.removeInvalidEnvVars($scope.formValues.Env);
const endpointId = +$state.params.endpointId;
@@ -221,20 +198,13 @@ angular
RepositoryAuthentication: $scope.formValues.RepositoryAuthentication,
RepositoryUsername: $scope.formValues.RepositoryUsername,
RepositoryPassword: $scope.formValues.RepositoryPassword,
+ AutoUpdate: transformAutoUpdateViewModel($scope.formValues.AutoUpdate),
};
- getAutoUpdatesProperty(repositoryOptions);
-
return StackService.createComposeStackFromGitRepository(name, repositoryOptions, env, endpointId);
}
}
- $scope.copyWebhook = function () {
- clipboard.copyText($scope.formValues.RepositoryWebhookURL);
- $('#copyNotification').show();
- $('#copyNotification').fadeOut(2000);
- };
-
$scope.handleEnvVarChange = handleEnvVarChange;
function handleEnvVarChange(value) {
$scope.formValues.Env = value;
@@ -348,6 +318,7 @@ angular
async function initView() {
var endpointMode = $scope.applicationState.endpoint.mode;
$scope.state.StackType = 2;
+ $scope.isDockerStandalone = endpointMode.provider === 'DOCKER_STANDALONE';
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER') {
$scope.state.StackType = 1;
}
@@ -369,11 +340,13 @@ angular
initView();
- function onChangeFormValues(values) {
- $scope.formValues = {
- ...$scope.formValues,
- ...values,
- };
+ function onChangeFormValues(newValues) {
+ return $async(async () => {
+ $scope.formValues = {
+ ...$scope.formValues,
+ ...newValues,
+ };
+ });
}
}
);
diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html
index a64828a87..3520b20b7 100644
--- a/app/portainer/views/stacks/create/createstack.html
+++ b/app/portainer/views/stacks/create/createstack.html
@@ -79,14 +79,14 @@
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts
index 0f1d478ab..5bcd4d6dc 100644
--- a/app/react-tools/test-mocks.ts
+++ b/app/react-tools/test-mocks.ts
@@ -102,7 +102,19 @@ export function createMockEnvironment(): Environment {
allowVolumeBrowserForRegularUsers: false,
enableHostManagementFeatures: false,
},
+ DeploymentOptions: {
+ overrideGlobalOptions: false,
+ hideAddWithForm: true,
+ hideWebEditor: false,
+ hideFileUpload: false,
+ },
Gpus: [],
Agent: { Version: '1.0.0' },
+ EnableImageNotification: false,
+ ChangeWindow: {
+ Enabled: false,
+ EndTime: '',
+ StartTime: '',
+ },
};
}
diff --git a/app/react/components/TimeWindowDisplay.tsx b/app/react/components/TimeWindowDisplay.tsx
new file mode 100644
index 000000000..8c49471b8
--- /dev/null
+++ b/app/react/components/TimeWindowDisplay.tsx
@@ -0,0 +1,87 @@
+import moment from 'moment';
+import 'moment-timezone';
+
+import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
+
+import { TextTip } from '@@/Tip/TextTip';
+
+import { withEdition } from '../portainer/feature-flags/withEdition';
+
+const TimeWindowDisplayWrapper = withEdition(TimeWindowDisplay, 'BE');
+
+export { TimeWindowDisplayWrapper as TimeWindowDisplay };
+
+function TimeWindowDisplay() {
+ const currentEnvQuery = useCurrentEnvironment(false);
+
+ if (!currentEnvQuery.data) {
+ return null;
+ }
+
+ const { ChangeWindow } = currentEnvQuery.data;
+
+ if (!ChangeWindow.Enabled) {
+ return null;
+ }
+ const timezone = moment.tz.guess();
+ const isDST = moment().isDST();
+ const { startTime: startTimeLocal, endTime: endTimeLocal } = utcToTime(
+ { startTime: ChangeWindow.StartTime, endTime: ChangeWindow.EndTime },
+ timezone
+ );
+
+ const { startTime: startTimeUtc, endTime: endTimeUtc } = parseInterval(
+ ChangeWindow.StartTime,
+ ChangeWindow.EndTime
+ );
+
+ return (
+
+ A change window is enabled, automatic updates will not occur outside of{' '}
+
+ {shortTime(startTimeUtc)} - {shortTime(endTimeUtc)} UTC (
+ {shortTime(startTimeLocal)} -{shortTime(endTimeLocal)}{' '}
+ {isDST ? 'DST' : ''} {timezone})
+
+ .
+
+ );
+}
+
+function utcToTime(
+ utcTime: { startTime: string; endTime: string },
+ timezone: string
+) {
+ const startTime = moment
+ .tz(utcTime.startTime, 'HH:mm', 'GMT')
+ .tz(timezone)
+ .format('HH:mm');
+ const endTime = moment
+ .tz(utcTime.endTime, 'HH:mm', 'GMT')
+ .tz(timezone)
+ .format('HH:mm');
+
+ return parseInterval(startTime, endTime);
+}
+
+function parseTime(originalTime: string) {
+ const [startHour, startMin] = originalTime.split(':');
+
+ const time = new Date();
+
+ time.setHours(parseInt(startHour, 10));
+ time.setMinutes(parseInt(startMin, 10));
+
+ return time;
+}
+
+function parseInterval(startTime: string, endTime: string) {
+ return {
+ startTime: parseTime(startTime),
+ endTime: parseTime(endTime),
+ };
+}
+
+function shortTime(time: Date) {
+ return moment(time).format('h:mm a');
+}
diff --git a/app/react/components/Tip/TextTip/TextTip.tsx b/app/react/components/Tip/TextTip/TextTip.tsx
index 5bf4a233e..aa18401cf 100644
--- a/app/react/components/Tip/TextTip/TextTip.tsx
+++ b/app/react/components/Tip/TextTip/TextTip.tsx
@@ -1,5 +1,6 @@
import { PropsWithChildren } from 'react';
import { AlertCircle } from 'lucide-react';
+import clsx from 'clsx';
import { Icon, IconMode } from '@@/Icon';
@@ -8,17 +9,18 @@ type Color = 'orange' | 'blue';
export interface Props {
icon?: React.ReactNode;
color?: Color;
+ className?: string;
}
export function TextTip({
color = 'orange',
icon = AlertCircle,
+ className,
children,
}: PropsWithChildren
) {
return (
-
-
-
+
+
{children}
);
diff --git a/app/react/components/Tip/Tooltip/Tooltip.tsx b/app/react/components/Tip/Tooltip/Tooltip.tsx
index 908e4f0e0..ee97c899c 100644
--- a/app/react/components/Tip/Tooltip/Tooltip.tsx
+++ b/app/react/components/Tip/Tooltip/Tooltip.tsx
@@ -1,12 +1,12 @@
import { HelpCircle } from 'lucide-react';
-import { useMemo } from 'react';
+import { ReactNode, useMemo } from 'react';
import sanitize from 'sanitize-html';
import { TooltipWithChildren, Position } from '../TooltipWithChildren';
export interface Props {
position?: Position;
- message: string;
+ message: ReactNode;
className?: string;
setHtmlMessage?: boolean;
}
@@ -19,7 +19,7 @@ export function Tooltip({
}: Props) {
// allow angular views to set html messages for the tooltip
const htmlMessage = useMemo(() => {
- if (setHtmlMessage) {
+ if (setHtmlMessage && typeof message === 'string') {
// eslint-disable-next-line react/no-danger
return ;
}
diff --git a/app/react/components/buttons/ButtonGroup.tsx b/app/react/components/buttons/ButtonGroup.tsx
index 9fd134d11..94d296911 100644
--- a/app/react/components/buttons/ButtonGroup.tsx
+++ b/app/react/components/buttons/ButtonGroup.tsx
@@ -21,6 +21,8 @@ export function ButtonGroup({
function sizeClass(size: Size | undefined) {
switch (size) {
+ case 'small':
+ return 'btn-group-sm';
case 'xsmall':
return 'btn-group-xs';
case 'large':
diff --git a/app/react/components/buttons/CopyButton/CopyButton.module.css b/app/react/components/buttons/CopyButton/CopyButton.module.css
index ebfdd3fc9..94607aeed 100644
--- a/app/react/components/buttons/CopyButton/CopyButton.module.css
+++ b/app/react/components/buttons/CopyButton/CopyButton.module.css
@@ -6,12 +6,10 @@
.container {
display: flex;
align-items: baseline;
- margin-top: 10px;
}
-.display-text {
+.copy-button {
opacity: 0;
- margin-left: 7px;
color: #23ae89;
}
diff --git a/app/react/components/buttons/CopyButton/CopyButton.tsx b/app/react/components/buttons/CopyButton/CopyButton.tsx
index 1511455bf..114b1796b 100644
--- a/app/react/components/buttons/CopyButton/CopyButton.tsx
+++ b/app/react/components/buttons/CopyButton/CopyButton.tsx
@@ -1,4 +1,4 @@
-import { PropsWithChildren } from 'react';
+import { ComponentProps, PropsWithChildren } from 'react';
import clsx from 'clsx';
import { Check, Copy } from 'lucide-react';
@@ -14,6 +14,7 @@ export interface Props {
fadeDelay?: number;
displayText?: string;
className?: string;
+ color?: ComponentProps['color'];
}
export function CopyButton({
@@ -21,6 +22,7 @@ export function CopyButton({
fadeDelay = 1000,
displayText = 'copied',
className,
+ color,
children,
}: PropsWithChildren) {
const { handleCopy, copiedSuccessfully } = useCopy(copyText, fadeDelay);
@@ -29,19 +31,20 @@ export function CopyButton({
) {
return (
-
+
);
}
diff --git a/app/react/components/form-components/FormControl/FormControl.tsx b/app/react/components/form-components/FormControl/FormControl.tsx
index ae71c283c..017f627e1 100644
--- a/app/react/components/form-components/FormControl/FormControl.tsx
+++ b/app/react/components/form-components/FormControl/FormControl.tsx
@@ -1,4 +1,4 @@
-import { PropsWithChildren, ReactNode } from 'react';
+import { ComponentProps, PropsWithChildren, ReactNode } from 'react';
import clsx from 'clsx';
import { Tooltip } from '@@/Tip/Tooltip';
@@ -11,11 +11,12 @@ export interface Props {
inputId?: string;
label: ReactNode;
size?: Size;
- tooltip?: string;
+ tooltip?: ComponentProps['message'];
+ setTooltipHtmlMessage?: ComponentProps['setHtmlMessage'];
children: ReactNode;
errors?: ReactNode;
required?: boolean;
- setTooltipHtmlMessage?: boolean;
+ className?: string;
}
export function FormControl({
@@ -25,12 +26,14 @@ export function FormControl({
tooltip = '',
children,
errors,
- required,
+ className,
+ required = false,
setTooltipHtmlMessage,
}: PropsWithChildren) {
return (
{children}
-
- {errors && (
-
- {errors}
-
- )}
+ {errors && {errors}}
);
diff --git a/app/react/components/form-components/FormError.tsx b/app/react/components/form-components/FormError.tsx
index 97e1f2cc6..8b96a89e5 100644
--- a/app/react/components/form-components/FormError.tsx
+++ b/app/react/components/form-components/FormError.tsx
@@ -10,7 +10,9 @@ interface Props {
export function FormError({ children, className }: PropsWithChildren) {
return (
-
+
{children}
diff --git a/app/react/components/form-components/Input/Input.tsx b/app/react/components/form-components/Input/Input.tsx
index 0a3aded0c..c1e087625 100644
--- a/app/react/components/form-components/Input/Input.tsx
+++ b/app/react/components/form-components/Input/Input.tsx
@@ -1,14 +1,24 @@
import clsx from 'clsx';
-import { InputHTMLAttributes } from 'react';
+import { forwardRef, InputHTMLAttributes, Ref } from 'react';
+
+export const InputWithRef = forwardRef<
+ HTMLInputElement,
+ InputHTMLAttributes
+>(
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ (props, ref) =>
+);
export function Input({
className,
+ mRef: ref,
...props
-}: InputHTMLAttributes) {
+}: InputHTMLAttributes & { mRef?: Ref }) {
return (
);
diff --git a/app/react/components/form-components/InputList/index.ts b/app/react/components/form-components/InputList/index.ts
index 6d613f13a..8be0fcfc8 100644
--- a/app/react/components/form-components/InputList/index.ts
+++ b/app/react/components/form-components/InputList/index.ts
@@ -1 +1 @@
-export { InputList } from './InputList';
+export { InputList, type ItemProps } from './InputList';
diff --git a/app/react/components/form-components/SwitchField/SwitchField.tsx b/app/react/components/form-components/SwitchField/SwitchField.tsx
index b2e958a8a..c65276dff 100644
--- a/app/react/components/form-components/SwitchField/SwitchField.tsx
+++ b/app/react/components/form-components/SwitchField/SwitchField.tsx
@@ -1,4 +1,5 @@
import clsx from 'clsx';
+import { ComponentProps } from 'react';
import { FeatureId } from '@/react/portainer/feature-flags/enums';
@@ -13,14 +14,14 @@ export interface Props {
onChange(value: boolean): void;
name?: string;
- tooltip?: string;
+ tooltip?: ComponentProps['message'];
+ setTooltipHtmlMessage?: ComponentProps['setHtmlMessage'];
labelClass?: string;
switchClass?: string;
fieldClass?: string;
dataCy?: string;
disabled?: boolean;
featureId?: FeatureId;
- setTooltipHtmlMessage?: boolean;
}
export function SwitchField({
diff --git a/app/react/components/form-components/useCachedTest.ts b/app/react/components/form-components/useCachedTest.ts
new file mode 100644
index 000000000..14dfbfc16
--- /dev/null
+++ b/app/react/components/form-components/useCachedTest.ts
@@ -0,0 +1,27 @@
+import { useRef } from 'react';
+import { TestContext, TestFunction } from 'yup';
+
+function cacheTest(
+ asyncValidate: TestFunction
+): TestFunction {
+ let valid = true;
+ let value: T | undefined;
+
+ return async (newValue: T, context: TestContext) => {
+ if (newValue !== value) {
+ value = newValue;
+
+ const response = await asyncValidate.call(context, newValue, context);
+ valid = !!response;
+ }
+ return valid;
+ };
+}
+
+export function useCachedValidation(
+ test: TestFunction
+) {
+ const ref = useRef(cacheTest(test));
+
+ return ref.current;
+}
diff --git a/app/react/components/form-components/useCaretPosition.ts b/app/react/components/form-components/useCaretPosition.ts
new file mode 100644
index 000000000..cf0eb9146
--- /dev/null
+++ b/app/react/components/form-components/useCaretPosition.ts
@@ -0,0 +1,26 @@
+import { useRef, useState, useCallback, useEffect } from 'react';
+
+export function useCaretPosition<
+ T extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement
+>() {
+ const node = useRef(null);
+ const [start, setStart] = useState(0);
+ const [end, setEnd] = useState(0);
+
+ const updateCaret = useCallback(() => {
+ if (node.current) {
+ const { selectionStart, selectionEnd } = node.current;
+
+ setStart(selectionStart || 0);
+ setEnd(selectionEnd || 0);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (node.current) {
+ node.current.setSelectionRange(start, end);
+ }
+ });
+
+ return { start, end, ref: node, updateCaret };
+}
diff --git a/app/react/components/form-components/validate-form.ts b/app/react/components/form-components/validate-form.ts
new file mode 100644
index 000000000..0d3e54450
--- /dev/null
+++ b/app/react/components/form-components/validate-form.ts
@@ -0,0 +1,19 @@
+import { yupToFormErrors } from 'formik';
+import { SchemaOf } from 'yup';
+
+export async function validateForm(
+ schemaBuilder: () => SchemaOf,
+ formValues: T
+) {
+ const validationSchema = schemaBuilder();
+
+ try {
+ await validationSchema.validate(formValues, {
+ strict: true,
+ abortEarly: false,
+ });
+ return undefined;
+ } catch (error) {
+ return yupToFormErrors(error);
+ }
+}
diff --git a/app/react/docker/stacks/types.ts b/app/react/docker/stacks/types.ts
index 62c2246f7..062028962 100644
--- a/app/react/docker/stacks/types.ts
+++ b/app/react/docker/stacks/types.ts
@@ -1,3 +1,5 @@
+export type StackId = number;
+
export enum StackType {
/**
* Represents a stack managed via docker stack
diff --git a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
index 6e4fc756e..7b0a8c57f 100644
--- a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
+++ b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx
@@ -58,7 +58,9 @@ export function ScriptTabs({
children: (
<>
{cmd}
- Copy
+
+ Copy
+
>
),
};
diff --git a/app/react/hooks/useCurrentEnvironment.ts b/app/react/hooks/useCurrentEnvironment.ts
index 70a2c9ada..7beaf6fb6 100644
--- a/app/react/hooks/useCurrentEnvironment.ts
+++ b/app/react/hooks/useCurrentEnvironment.ts
@@ -1,8 +1,8 @@
-import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment';
+import { useEnvironment } from '@/react/portainer/environments/queries';
import { useEnvironmentId } from './useEnvironmentId';
-export function useCurrentEnvironment() {
- const id = useEnvironmentId();
+export function useCurrentEnvironment(force = true) {
+ const id = useEnvironmentId(force);
return useEnvironment(id);
}
diff --git a/app/react/hooks/useDebounce.ts b/app/react/hooks/useDebounce.ts
index 24c9c5535..3e413417e 100644
--- a/app/react/hooks/useDebounce.ts
+++ b/app/react/hooks/useDebounce.ts
@@ -1,21 +1,22 @@
import _ from 'lodash';
-import { useState, useRef, useCallback } from 'react';
+import { useState, useRef, useCallback, useEffect } from 'react';
-export function useDebounce(
- defaultValue: string,
- onChange: (value: string) => void
-) {
- const [searchValue, setSearchValue] = useState(defaultValue);
+export function useDebounce(value: string, onChange: (value: string) => void) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
const onChangeDebounces = useRef(_.debounce(onChange, 300));
const handleChange = useCallback(
(value: string) => {
- setSearchValue(value);
+ setDebouncedValue(value);
onChangeDebounces.current(value);
},
- [onChangeDebounces, setSearchValue]
+ [onChangeDebounces, setDebouncedValue]
);
- return [searchValue, handleChange] as const;
+ useEffect(() => {
+ setDebouncedValue(value);
+ }, [value]);
+
+ return [debouncedValue, handleChange] as const;
}
diff --git a/app/react/hooks/useEnvironmentId.ts b/app/react/hooks/useEnvironmentId.ts
index c3fdd1c4b..7ba1667b8 100644
--- a/app/react/hooks/useEnvironmentId.ts
+++ b/app/react/hooks/useEnvironmentId.ts
@@ -1,13 +1,19 @@
import { useCurrentStateAndParams } from '@uirouter/react';
-export function useEnvironmentId() {
+import { EnvironmentId } from '@/react/portainer/environments/types';
+
+export function useEnvironmentId(force = true): EnvironmentId {
const {
params: { endpointId: environmentId },
} = useCurrentStateAndParams();
if (!environmentId) {
+ if (!force) {
+ return 0;
+ }
+
throw new Error('endpointId url param is required');
}
- return environmentId;
+ return parseInt(environmentId, 10);
}
diff --git a/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx b/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx
index f99cae8a1..61a454242 100644
--- a/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx
+++ b/app/react/portainer/account/CreateAccessTokenView/CreateAccessToken.tsx
@@ -94,9 +94,11 @@ export function CreateAccessToken({
{accessToken}
-
- Copy access token
-
+
+
+ Copy access token
+
+