Compare commits

..

24 Commits

Author SHA1 Message Date
Steven Kang
14b998d270 Set static DOCKER_VERSION for ppc64le and s390x (#7135) 2022-06-28 11:38:12 +12:00
Chaim Lev-Ari
605ff8c1da fix(environments): hide async mode on deployment [EE-3380] (#7129)
fixes [EE-3380]
2022-06-28 10:23:07 +12:00
Chaim Lev-Ari
13f93f4262 fix(analytics): load public settings [EE-3590] (#7127) 2022-06-27 19:29:06 +03:00
Steven Kang
16be5ed329 feat(build): set static DOCKER_VERSION for ppc64le and s390x (#7124) 2022-06-27 09:54:04 +12:00
Chaim Lev-Ari
c6612898f3 fix(api): add missing edge types [EE-3590] (#7117) 2022-06-26 08:38:20 +03:00
andres-portainer
564f34b0ba fix(wizard): replace the YAML file by the docker commands EE-3589 (#7112) 2022-06-24 14:59:00 -03:00
LP B
392fbdb4a7 fix(app/account): ensure newTransition exists in uiCanExit [EE-3336] (#7109) 2022-06-24 17:35:39 +02:00
Chaim Lev-Ari
a826c78786 fix(edge): show heartbeat for async env [EE-3380] (#7096) 2022-06-22 20:11:42 +03:00
Matt Hook
a35f0607f1 fix docker download path for mac platforms (#7101) 2022-06-22 10:06:28 +12:00
LP B
081d32af0d fix(app/account): create access token button (#7091)
* fix(app/account): create access token button

* fix(app/formcontrol): error message overlapping input on smaller screens
2022-06-20 14:14:41 +02:00
itsconquest
4cc0b1f567 fix(auth): track skips per user [EE-3318] (#7088) 2022-06-20 17:00:00 +12:00
Chaim Lev-Ari
d4da7e1760 fix(docker/networks): show correct resource control data [EE-3401] (#7061) 2022-06-17 19:21:38 +03:00
itsconquest
aced418880 fix(auth): clear skips when using new instance [EE-3331] (#7026) 2022-06-17 14:45:42 +12:00
Chaim Lev-Ari
614f42fe5a feat(custom-templates): hide variables [EE-2602] (#7069) 2022-06-16 08:32:43 +03:00
itsconquest
58736fe93b feat(auth): allow single char passwords [EE-3385] (#7049)
* feat(auth): allow single character passwords

* match weak password modal logic to slider
2022-06-16 12:31:39 +12:00
Matt Hook
b78330b10d fix(swarm): don't stomp on the x-registry-auth header EE-3308 (#7037)
* don't stomp on the x-registry-auth header

* del header if empty json provided for registry auth
2022-06-16 09:54:06 +12:00
itsconquest
eed4a92ca8 fix(auth): notify user password requirements [EE-3344] (#7041)
* fix(auth): notify user password requirements [EE-3344]

* fix angular code
2022-06-15 17:15:38 +12:00
Dmitry Salakhov
0e7468a1e8 fix: clarify password change error (#7020) 2022-06-15 15:44:54 +12:00
congs
b807481f1c fix(teamleader): EE-3411 normal users get an unauthorized error (#7053) 2022-06-14 14:12:33 +12:00
Ali
da27de2154 fix(wizard): return back to envs page EE-3419 (#7064) 2022-06-13 14:59:23 +12:00
congs
6743e4fbb2 fix(teamleader): EE-3383 allow teamleader promote member to teamleader (#7039) 2022-06-10 17:13:23 +12:00
Ali
b489ffaa63 fix(wizard): show teasers for kaas and kubeconfig features [EE-3316] (#7033)
* fix(wizard): add nomad, kaas, kubeconfig teasers
2022-06-10 09:16:43 +12:00
congs
6e12499d61 fix(teamleader): EE-3332 hide name and leaders (#7032) 2022-06-09 14:22:42 +12:00
Ali
f7acbe16ba fix(wizard): use 'New Environments' title EE-3329 (#7035) 2022-06-08 16:37:53 +12:00
67 changed files with 547 additions and 569 deletions

View File

@@ -22,7 +22,7 @@ Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
**The latest version of Portainer is 2.13.x**.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
## Getting started

View File

@@ -35,6 +35,12 @@
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"GroupId": 1,
@@ -682,6 +688,12 @@
"BlackListedLabels": [],
"DisplayDonationHeader": false,
"DisplayExternalContributors": false,
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
@@ -898,7 +910,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "60",
"DB_VERSION": "50",
"INSTANCE_ID": "null"
}
}

View File

@@ -80,7 +80,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.15.0
// @version 2.14.0
// @description.markdown api-description.md
// @termsOfService

View File

@@ -30,6 +30,19 @@ type publicSettingsResponse struct {
KubeconfigExpiry string `example:"24h" default:"0"`
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
Edge struct {
// Whether the device has been started in edge async mode
AsyncMode bool
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
// The check in interval for edge agent (in seconds) - used in non async mode [seconds]
CheckinInterval int `example:"60"`
}
}
// @id SettingsPublic
@@ -61,6 +74,13 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: appSettings.FeatureFlagSettings,
}
publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI

View File

@@ -23,7 +23,7 @@ type (
}
portainerRegistryAuthenticationHeader struct {
RegistryId portainer.RegistryID `json:"registryId"`
RegistryId *portainer.RegistryID `json:"registryId"`
}
)

View File

@@ -446,7 +446,20 @@ func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.R
return err
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
// delete header and exist function without error if Front End
// passes empty json. This is to restore original behavior which
// never originally passed this header
if string(decodedHeaderData) == "{}" {
request.Header.Del("X-Registry-Auth")
return nil
}
// only set X-Registry-Auth if registryId is defined
if originalHeaderData.RegistryId == nil {
return nil
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
if err != nil {
return err
}

View File

@@ -37,7 +37,7 @@ func parseRegToken(registry *portainer.Registry) (username, password string, err
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
if registry.Type == portainer.EcrRegistry {
if isRegTokenValid(registry) {
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: current ECR token is still valid]")
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: curretn ECR token is still valid]")
} else {
err = doGetRegToken(dataStore, registry)
if err != nil {

View File

@@ -345,6 +345,17 @@ type (
// Whether the device has been trusted or not by the user
UserTrusted bool
Edge struct {
// Whether the device has been started in edge async mode
AsyncMode bool
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
}
// Deprecated fields
// Deprecated in DBVersion == 4
TLS bool `json:"TLS,omitempty"`
@@ -837,6 +848,17 @@ type (
// EdgePortainerURL is the URL that is exposed to edge agents
EdgePortainerURL string `json:"EdgePortainerUrl"`
Edge struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
// EdgeAsyncMode enables edge async mode by default
AsyncMode bool
}
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
@@ -1363,9 +1385,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.15.0"
APIVersion = "2.14.0"
// DBVersion is the version number of the Portainer database
DBVersion = 60
DBVersion = 50
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server

View File

@@ -1,6 +1,6 @@
import _ from 'lodash';
import { useSettings } from '@/portainer/settings/queries';
import { usePublicSettings } from '@/portainer/settings/queries';
const categories = [
'docker',
@@ -64,7 +64,9 @@ export function push(
}
export function useAnalytics() {
const telemetryQuery = useSettings((settings) => settings.EnableTelemetry);
const telemetryQuery = usePublicSettings({
select: (settings) => settings.EnableTelemetry,
});
return { trackEvent: handleTrackEvent };

View File

@@ -1,7 +1,5 @@
import {
AccessControlFormData,
ResourceControlResponse,
} from '@/portainer/access-control/types';
import { AccessControlFormData } from '@/portainer/access-control/types';
import { PortainerMetadata } from '@/react/docker/types';
import { PortMapping } from './ContainerInstances/CreateContainerInstanceForm/PortsMappingField';
@@ -21,10 +19,6 @@ export interface ContainerInstanceFormValues {
accessControl: AccessControlFormData;
}
interface PortainerMetadata {
ResourceControl: ResourceControlResponse;
}
interface Container {
name: string;
properties: {

View File

@@ -1,7 +1,6 @@
import { render } from '@/react-tools/test-utils';
import { UserContext } from '@/portainer/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user';
import { ResourceControlOwnership } from '@/portainer/access-control/types';
import { DockerNetwork } from '../types';
@@ -113,9 +112,9 @@ function getNetwork(networkName: string): DockerNetwork {
},
],
TeamAccesses: [],
Ownership: ResourceControlOwnership.PUBLIC,
Public: true,
System: false,
AdministratorsOnly: true,
},
},
Scope: 'local',

View File

@@ -9,6 +9,7 @@ import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm
import { AccessControlPanel } from '@/portainer/access-control/AccessControlPanel/AccessControlPanel';
import { ResourceControlType } from '@/portainer/access-control/types';
import { DockerContainer } from '@/docker/containers/types';
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
import { useNetwork, useDeleteNetwork } from '../queries';
import { isSystemNetwork } from '../network.helper';
@@ -50,6 +51,12 @@ export function NetworkDetailsView() {
return null;
}
const network = networkQuery.data;
const resourceControl = network.Portainer?.ResourceControl
? new ResourceControlViewModel(network.Portainer.ResourceControl)
: undefined;
return (
<>
<PageHeader
@@ -77,7 +84,7 @@ export function NetworkDetailsView() {
networkId,
])
}
resourceControl={networkQuery.data.Portainer?.ResourceControl}
resourceControl={resourceControl}
resourceType={ResourceControlType.Network}
disableOwnershipChange={isSystemNetwork(networkQuery.data.Name)}
resourceId={networkId}

View File

@@ -1,4 +1,4 @@
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
import { PortainerMetadata } from '@/react/docker/types';
import { ContainerId } from '../containers/types';
@@ -44,7 +44,7 @@ export interface DockerNetwork {
Driver: string;
Options: IpamOptions;
};
Portainer: { ResourceControl?: ResourceControlViewModel };
Portainer?: PortainerMetadata;
Options: NetworkOptions;
Containers: NetworkResponseContainers;
}

View File

@@ -27,13 +27,6 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
$scope.state.isEditorDirty = false;
});
$scope.checkName = function (name) {
const parts = name.split('/');
const repository = parts[parts.length - 1];
const repositoryRegExp = RegExp('^[a-z0-9-_]{2,255}(:[A-Za-z0-9-_.]{1,128})?$');
return repositoryRegExp.test(repository);
};
$scope.addImageName = function () {
$scope.formValues.ImageNames.push({ Name: '' });
};
@@ -99,16 +92,13 @@ function BuildImageController($scope, $async, $window, ModalService, BuildServic
}
$scope.validImageNames = function () {
if ($scope.formValues.ImageNames.length == 0) {
return false;
}
for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
var item = $scope.formValues.ImageNames[i];
if (!$scope.checkName(item.Name)) {
return false;
if (item.Name !== '') {
return true;
}
}
return true;
return false;
};
$scope.editorUpdate = function (cm) {

View File

@@ -47,7 +47,7 @@
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="item.Name" placeholder="e.g. my-image:my-tag" auto-focus />
<span class="input-group-addon"
><i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[checkName(item.Name)]" aria-hidden="true"></i
><i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[item.Name !== '']" aria-hidden="true"></i
></span>
</div>
<!-- !name-input -->

View File

@@ -15,12 +15,5 @@ export const heartbeat: Column<Environment> = {
export function StatusCell({
row: { original: environment },
}: CellProps<Environment>) {
return (
<EdgeIndicator
checkInInterval={environment.EdgeCheckinInterval}
edgeId={environment.EdgeID}
lastCheckInDate={environment.LastCheckInDate}
queryDate={environment.QueryDate}
/>
);
return <EdgeIndicator environment={environment} />;
}

View File

@@ -4,6 +4,8 @@ import { TableSettingsProvider } from '@/portainer/components/datatables/compone
import { PageHeader } from '@/portainer/components/PageHeader';
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
import { r2a } from '@/react-tools/react2angular';
import { InformationPanel } from '@/portainer/components/InformationPanel';
import { TextTip } from '@/portainer/components/Tip/TextTip';
import { DataTable } from './Datatable/Datatable';
import { TableSettings } from './Datatable/types';
@@ -29,6 +31,15 @@ export function WaitingRoomView() {
{ label: 'Waiting Room' },
]}
/>
<InformationPanel>
<TextTip color="blue">
Only environments generated from the AEEC script will appear here,
manually added environments and edge devices will bypass the waiting
room.
</TextTip>
</InformationPanel>
<TableSettingsProvider<TableSettings>
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
storageKey={storageKey}

View File

@@ -1,6 +1,7 @@
import { buildOption } from '@/portainer/components/BoxSelector';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
class KubeCreateCustomTemplateViewController {
/* @ngInject */
@@ -13,6 +14,7 @@ class KubeCreateCustomTemplateViewController {
];
this.templates = null;
this.isTemplateVariablesEnabled = isBE;
this.state = {
method: 'editor',
@@ -53,6 +55,10 @@ class KubeCreateCustomTemplateViewController {
}
parseTemplate(templateStr) {
if (!this.isTemplateVariablesEnabled) {
return;
}
const variables = getTemplateVariables(templateStr);
const isValid = !!variables;

View File

@@ -37,6 +37,7 @@
</file-upload-form>
<custom-templates-variables-definition-field
ng-if="$ctrl.isTemplateVariablesEnabled"
value="$ctrl.formValues.Variables"
on-change="($ctrl.onVariablesChange)"
is-variables-names-from-parent="$ctrl.state.method === 'editor'"

View File

@@ -1,5 +1,6 @@
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
class KubeEditCustomTemplateViewController {
@@ -7,6 +8,8 @@ class KubeEditCustomTemplateViewController {
constructor($async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
this.isTemplateVariablesEnabled = isBE;
this.formValues = null;
this.state = {
formValidationError: '',
@@ -60,6 +63,10 @@ class KubeEditCustomTemplateViewController {
}
parseTemplate(templateStr) {
if (!this.isTemplateVariablesEnabled) {
return;
}
const variables = getTemplateVariables(templateStr);
const isValid = !!variables;

View File

@@ -32,6 +32,7 @@
</web-editor-form>
<custom-templates-variables-definition-field
ng-if="$ctrl.isTemplateVariablesEnabled"
value="$ctrl.formValues.Variables"
on-change="($ctrl.onVariablesChange)"
is-variables-names-from-parent="true"

View File

@@ -300,8 +300,8 @@
</div>
<div class="col-sm-12 small text-muted" style="margin-top: 15px" ng-if="ctrl.formValues.Configurations.length">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px"></i>
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overridden to filesystem mounts for each
key via the override button.
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key
via the override button.
</div>
</div>

View File

@@ -93,7 +93,7 @@
></custom-template-selector>
<custom-templates-variables-field
ng-if="ctrl.state.template"
ng-if="$ctrl.isTemplateVariablesEnabled && ctrl.state.template"
definitions="ctrl.state.template.Variables"
value="ctrl.formValues.Variables"
on-change="(ctrl.onChangeTemplateVariables)"

View File

@@ -7,6 +7,7 @@ import PortainerError from '@/portainer/error';
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
import { buildOption } from '@/portainer/components/BoxSelector';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
class KubernetesDeployController {
/* @ngInject */
@@ -23,6 +24,8 @@ class KubernetesDeployController {
this.CustomTemplateService = CustomTemplateService;
this.DeployMethod = 'manifest';
this.isTemplateVariablesEnabled = isBE;
this.deployOptions = [
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
buildOption('method_compose', 'fab fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
@@ -83,6 +86,10 @@ class KubernetesDeployController {
}
renderTemplate() {
if (!this.isTemplateVariablesEnabled) {
return;
}
const rendered = renderTemplate(this.state.templateContent, this.formValues.Variables, this.state.template.Variables);
this.onChangeFormValues({ EditorContent: rendered });
}

View File

@@ -33,11 +33,11 @@ export class ResourceControlViewModel {
this.TeamAccesses = data.TeamAccesses;
this.Public = data.Public;
this.System = data.System;
this.Ownership = determineOwnership(this);
this.Ownership = determineOwnership(data);
}
}
function determineOwnership(resourceControl: ResourceControlViewModel) {
function determineOwnership(resourceControl: ResourceControlResponse) {
if (resourceControl.Public) {
return ResourceControlOwnership.PUBLIC;
}

View File

@@ -4,7 +4,7 @@ import { Button } from '../Button';
import { Widget, WidgetBody } from '../widget';
interface Props {
title: string;
title?: string;
onDismiss?(): void;
bodyClassName?: string;
wrapperStyle?: Record<string, string>;
@@ -23,21 +23,23 @@ export function InformationPanel({
<Widget>
<WidgetBody className={bodyClassName}>
<div style={wrapperStyle}>
<div className="col-sm-12 form-section-title">
<span style={{ float: 'left' }}>{title}</span>
{!!onDismiss && (
<span
className="small"
style={{ float: 'right' }}
ng-if="dismissAction"
>
<Button color="link" onClick={() => onDismiss()}>
<i className="fa fa-times" /> dismiss
</Button>
</span>
)}
</div>
<div className="form-group">{children}</div>
{title && (
<div className="col-sm-12 form-section-title">
<span style={{ float: 'left' }}>{title}</span>
{!!onDismiss && (
<span
className="small"
style={{ float: 'right' }}
ng-if="dismissAction"
>
<Button color="link" onClick={() => onDismiss()}>
<i className="fa fa-times" /> dismiss
</Button>
</span>
)}
</div>
)}
<div>{children}</div>
</div>
</WidgetBody>
</Widget>

View File

@@ -3,7 +3,7 @@
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<div class="toolBar small" ng-if="$ctrl.inheritFrom">
Access tagged as <code>inherited</code> are inherited from the group access. They cannot be removed or modified at the environment level but they can be overridden.
Access tagged as <code>inherited</code> are inherited from the group access. They cannot be removed or modified at the environment level but they can be overriden.
</div>
<div class="toolBar small" ng-if="$ctrl.inheritFrom"> Access tagged as <code>override</code> are overriding the group access for the related users/teams. </div>
<div class="actionBar">

View File

@@ -29,26 +29,29 @@ export function FormControl({
required,
}: PropsWithChildren<Props>) {
return (
<div className={clsx('form-group', styles.container)}>
<label
htmlFor={inputId}
className={clsx(sizeClassLabel(size), 'control-label', 'text-left')}
>
{label}
<>
<div className={clsx('form-group', styles.container)}>
<label
htmlFor={inputId}
className={clsx(sizeClassLabel(size), 'control-label', 'text-left')}
>
{label}
{required && <span className="text-danger">*</span>}
{required && <span className="text-danger">*</span>}
{tooltip && <Tooltip message={tooltip} />}
</label>
<div className={sizeClassChildren(size)}>{children}</div>
{tooltip && <Tooltip message={tooltip} />}
</label>
<div className={sizeClassChildren(size)}>{children}</div>
</div>
{errors && (
<div className="col-md-12">
<FormError>{errors}</FormError>
<div className="form-group">
<div className="col-md-12">
<FormError>{errors}</FormError>
</div>
</div>
)}
</div>
</>
);
}

View File

@@ -41,7 +41,7 @@ class KubernetesAppGitFormController {
try {
const confirmed = await this.ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
message: 'Any changes to this application will be overriden by the definition in git and may cause a service interruption. Do you wish to continue?',
buttons: {
confirm: {
label: 'Update',

View File

@@ -82,7 +82,7 @@ class KubernetesRedeployAppGitFormController {
try {
const confirmed = await this.ModalService.confirmAsync({
title: 'Are you sure?',
message: 'Any changes to this application will be overridden by the definition in git and may cause a service interruption. Do you wish to continue?',
message: 'Any changes to this application will be overriden by the definition in git and may cause a service interruption. Do you wish to continue?',
buttons: {
confirm: {
label: 'Update',

View File

@@ -54,6 +54,13 @@ export interface KubernetesSettings {
Snapshots?: KubernetesSnapshot[] | null;
}
export type EnvironmentEdge = {
AsyncMode: boolean;
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
};
export type Environment = {
Id: EnvironmentId;
Type: EnvironmentType;
@@ -73,6 +80,7 @@ export type Environment = {
IsEdgeDevice?: boolean;
UserTrusted: boolean;
AMTDeviceGUID?: string;
Edge: EnvironmentEdge;
};
/**
* TS reference of endpoint_create.go#EndpointCreationType iota

View File

@@ -1,5 +1,6 @@
import { Edition, FeatureId, FeatureState } from './enums';
export const isBE = process.env.PORTAINER_EDITION === 'BE';
interface ServiceState {
currentEdition: Edition;
features: Record<FeatureId, Edition>;

View File

@@ -1,35 +1,44 @@
import { render } from '@/react-tools/test-utils';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { renderWithQueryClient } from '@/react-tools/test-utils';
import { rest, server } from '@/setup-tests/server';
import { EdgeIndicator } from './EdgeIndicator';
test('when edge id is not set, should show unassociated label', () => {
const { queryByLabelText } = renderComponent();
test('when edge id is not set, should show unassociated label', async () => {
const { queryByLabelText } = await renderComponent();
const unassociatedLabel = queryByLabelText('unassociated');
expect(unassociatedLabel).toBeVisible();
});
test('given edge id and last checkin is set, should show heartbeat', () => {
const { queryByLabelText } = renderComponent('id', 1);
// test('given edge id and last checkin is set, should show heartbeat', async () => {
// const { queryByLabelText } = await renderComponent('id', 1);
expect(queryByLabelText('edge-heartbeat')).toBeVisible();
expect(queryByLabelText('edge-last-checkin')).toBeVisible();
});
// expect(queryByLabelText('edge-heartbeat')).toBeVisible();
// expect(queryByLabelText('edge-last-checkin')).toBeVisible();
// });
function renderComponent(
async function renderComponent(
edgeId = '',
lastCheckInDate = 0,
checkInInterval = 0,
queryDate = 0
) {
return render(
<EdgeIndicator
edgeId={edgeId}
lastCheckInDate={lastCheckInDate}
checkInInterval={checkInInterval}
queryDate={queryDate}
showLastCheckInDate
/>
server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({}))));
const environment = createMockEnvironment();
environment.EdgeID = edgeId;
environment.LastCheckInDate = lastCheckInDate;
environment.EdgeCheckinInterval = checkInInterval;
environment.QueryDate = queryDate;
const queries = renderWithQueryClient(
<EdgeIndicator environment={environment} showLastCheckInDate />
);
await expect(queries.findByRole('status')).resolves.toBeVisible();
return queries;
}

View File

@@ -1,56 +1,114 @@
import clsx from 'clsx';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { Environment } from '@/portainer/environments/types';
import { usePublicSettings } from '@/portainer/settings/queries';
import { PublicSettingsViewModel } from '@/portainer/models/settings';
interface Props {
checkInInterval?: number;
edgeId?: string;
queryDate?: number;
lastCheckInDate?: number;
showLastCheckInDate?: boolean;
environment: Environment;
}
export function EdgeIndicator({
edgeId,
lastCheckInDate,
checkInInterval,
queryDate,
environment,
showLastCheckInDate = false,
}: Props) {
if (!edgeId) {
const associated = !!environment.EdgeID;
const isValid = useHasHeartbeat(environment, associated);
if (isValid === null) {
return null;
}
if (!associated) {
return (
<span className="label label-default" aria-label="unassociated">
<s>associated</s>
<span role="status" aria-label="edge-status">
<span className="label label-default" aria-label="unassociated">
<s>associated</s>
</span>
</span>
);
}
// give checkIn some wiggle room
let isCheckValid = false;
if (checkInInterval && queryDate && lastCheckInDate) {
isCheckValid = queryDate - lastCheckInDate <= checkInInterval * 2 + 20;
}
return (
<span>
<span role="status" aria-label="edge-status">
<span
className={clsx('label', {
'label-danger': !isCheckValid,
'label-success': isCheckValid,
'label-danger': !isValid,
'label-success': isValid,
})}
aria-label="edge-heartbeat"
>
heartbeat
</span>
{showLastCheckInDate && !!lastCheckInDate && (
{showLastCheckInDate && !!environment.LastCheckInDate && (
<span
className="space-left small text-muted"
aria-label="edge-last-checkin"
>
{isoDateFromTimestamp(lastCheckInDate)}
{isoDateFromTimestamp(environment.LastCheckInDate)}
</span>
)}
</span>
);
}
function useHasHeartbeat(environment: Environment, associated: boolean) {
const settingsQuery = usePublicSettings({ enabled: associated });
if (!associated) {
return false;
}
const { LastCheckInDate, QueryDate } = environment;
const settings = settingsQuery.data;
if (!settings) {
return null;
}
const checkInInterval = getCheckinInterval(environment, settings);
if (checkInInterval && QueryDate && LastCheckInDate) {
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
}
return false;
}
function getCheckinInterval(
environment: Environment,
settings: PublicSettingsViewModel
) {
const asyncMode = environment.Edge.AsyncMode;
if (asyncMode) {
const intervals = [
environment.Edge.PingInterval > 0
? environment.Edge.PingInterval
: settings.Edge.PingInterval,
environment.Edge.SnapshotInterval > 0
? environment.Edge.SnapshotInterval
: settings.Edge.SnapshotInterval,
environment.Edge.CommandInterval > 0
? environment.Edge.CommandInterval
: settings.Edge.CommandInterval,
].filter((n) => n > 0);
return intervals.length > 0 ? Math.min(...intervals) : 60;
}
if (
!environment.EdgeCheckinInterval ||
environment.EdgeCheckinInterval === 0
) {
return settings.Edge.CheckinInterval;
}
return environment.EdgeCheckinInterval;
}

View File

@@ -5,6 +5,7 @@ import {
EnvironmentStatus,
EnvironmentType,
} from '@/portainer/environments/types';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { EnvironmentItem } from './EnvironmentItem';
@@ -57,19 +58,9 @@ KubernetesEdgeEnvironment.args = {
};
function mockEnvironment(type: EnvironmentType): Environment {
return {
Id: 1,
Name: 'environment',
GroupId: 1,
Snapshots: [],
Status: EnvironmentStatus.Up,
TagIds: [],
Type: type,
Kubernetes: {
Snapshots: [],
},
URL: 'url',
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
env.Type = type;
env.Status = EnvironmentStatus.Up;
return env;
}

View File

@@ -6,25 +6,14 @@ import { Environment } from '@/portainer/environments/types';
import { UserContext } from '@/portainer/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user';
import { Tag } from '@/portainer/tags/types';
import { createMockEnvironment } from '@/react-tools/test-mocks';
import { renderWithQueryClient } from '@/react-tools/test-utils';
import { server, rest } from '@/setup-tests/server';
import { EnvironmentItem } from './EnvironmentItem';
test('loads component', async () => {
const env: Environment = {
TagIds: [],
GroupId: 1,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
const { getByText } = renderComponent(env);
expect(getByText(env.Name)).toBeInTheDocument();
@@ -34,19 +23,8 @@ test('shows group name', async () => {
const groupName = 'group-name';
const groupId: EnvironmentGroupId = 14;
const env: Environment = {
TagIds: [],
GroupId: groupId,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
Id: 3,
UserTrusted: false,
EdgeKey: '',
};
const env = createMockEnvironment();
env.GroupId = groupId;
const { findByText } = renderComponent(env, { Name: groupName });

View File

@@ -68,10 +68,8 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
<span className="space-left blocklist-item-subtitle">
{isEdge ? (
<EdgeIndicator
edgeId={environment.EdgeID}
checkInInterval={environment.EdgeCheckinInterval}
lastCheckInDate={environment.LastCheckInDate}
queryDate={environment.QueryDate}
environment={environment}
showLastCheckInDate
/>
) : (
<>

View File

@@ -302,19 +302,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
)}
</div>
<div className={styles.kubeconfigButton}>
<KubeconfigButton
environments={environments}
envQueryParams={{
types: platformType,
search: debouncedTextFilter,
status: statusFilter,
tagIds: tagFilter?.length ? tagFilter : undefined,
groupIds: groupFilter,
sort: sortByFilter,
order: sortByDescending ? 'desc' : 'asc',
edgeDeviceFilter: 'none',
}}
/>
<KubeconfigButton environments={environments} />
</div>
<div className={styles.filterSearchbar}>
<FilterSearchBar

View File

@@ -1,24 +1,16 @@
import { useState } from 'react';
import * as kcService from '@/kubernetes/services/kubeconfig.service';
import * as notifications from '@/portainer/services/notifications';
import { confirmKubeconfigSelection } from '@/portainer/services/modal.service/prompt';
import { Environment } from '@/portainer/environments/types';
import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service/index';
import { isKubernetesEnvironment } from '@/portainer/environments/utils';
import { trackEvent } from '@/angulartics.matomo/analytics-services';
import { Button } from '@/portainer/components/Button';
import { KubeconfigPrompt } from './KubeconfigPrompt';
import '@reach/dialog/styles.css';
export interface KubeconfigButtonProps {
environments: Environment[];
envQueryParams: EnvironmentsQueryParams;
interface Props {
environments?: Environment[];
}
export function KubeconfigButton({
environments,
envQueryParams,
}: KubeconfigButtonProps) {
const [isOpen, setIsOpen] = useState(false);
export function KubeconfigButton({ environments }: Props) {
if (!environments) {
return null;
}
@@ -28,12 +20,9 @@ export function KubeconfigButton({
}
return (
<>
<Button onClick={handleClick}>
<i className="fas fa-download space-right" /> kubeconfig
</Button>
{prompt()}
</>
<Button onClick={handleClick}>
<i className="fas fa-download space-right" /> kubeconfig
</Button>
);
function handleClick() {
@@ -45,28 +34,48 @@ export function KubeconfigButton({
category: 'kubernetes',
});
setIsOpen(true);
}
function handleClose() {
setIsOpen(false);
}
function isKubeconfigButtonVisible(environments: Environment[]) {
if (window.location.protocol !== 'https:') {
return false;
}
return environments.some((env) => isKubernetesEnvironment(env.Type));
}
function prompt() {
return (
isOpen && (
<KubeconfigPrompt
envQueryParams={envQueryParams}
onClose={handleClose}
/>
)
);
showKubeconfigModal(environments);
}
}
function isKubeconfigButtonVisible(environments: Environment[]) {
if (window.location.protocol !== 'https:') {
return false;
}
return environments.some((env) => isKubernetesEnvironment(env.Type));
}
async function showKubeconfigModal(environments: Environment[]) {
const kubeEnvironments = environments.filter((env) =>
isKubernetesEnvironment(env.Type)
);
const options = kubeEnvironments.map((environment) => ({
text: `${environment.Name} (${environment.URL})`,
value: `${environment.Id}`,
}));
let expiryMessage = '';
try {
expiryMessage = await kcService.expiryMessage();
} catch (e) {
notifications.error('Failed fetching kubeconfig expiry time', e as Error);
}
confirmKubeconfigSelection(
options,
expiryMessage,
async (selectedEnvironmentIDs: string[]) => {
if (selectedEnvironmentIDs.length === 0) {
notifications.warning('No environment was selected', '');
return;
}
try {
await kcService.downloadKubeconfigFile(
selectedEnvironmentIDs.map((id) => parseInt(id, 10))
);
} catch (e) {
notifications.error('Failed downloading kubeconfig file', e as Error);
}
}
);
}

View File

@@ -1,9 +0,0 @@
.checkbox {
padding-left: 0.5rem;
}
.dialog {
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -1,141 +0,0 @@
import { useState } from 'react';
import { useQuery } from 'react-query';
import { DialogOverlay } from '@reach/dialog';
import * as kcService from '@/kubernetes/services/kubeconfig.service';
import * as notifications from '@/portainer/services/notifications';
import { Button } from '@/portainer/components/Button';
import { Checkbox } from '@/portainer/components/form-components/Checkbox';
import { EnvironmentType } from '@/portainer/environments/types';
import { EnvironmentsQueryParams } from '@/portainer/environments/environment.service/index';
import { PaginationControls } from '@/portainer/components/pagination-controls';
import { usePaginationLimitState } from '@/portainer/hooks/usePaginationLimitState';
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
import { useSelection } from './KubeconfigSelection';
import styles from './KubeconfigPrompt.module.css';
import '@reach/dialog/styles.css';
export interface KubeconfigPromptProps {
envQueryParams: EnvironmentsQueryParams;
onClose: () => void;
}
const storageKey = 'home_endpoints';
export function KubeconfigPrompt({
envQueryParams,
onClose,
}: KubeconfigPromptProps) {
const [page, setPage] = useState(1);
const [pageLimit, setPageLimit] = usePaginationLimitState(storageKey);
const kubeServiceExpiryQuery = useQuery(['kubeServiceExpiry'], async () => {
const expiryMessage = await kcService.expiryMessage();
return expiryMessage;
});
const { selection, toggle: toggleSelection, selectionSize } = useSelection();
const { environments, totalCount } = useEnvironmentList({
...envQueryParams,
page,
pageLimit,
types: [
EnvironmentType.KubernetesLocal,
EnvironmentType.AgentOnKubernetes,
EnvironmentType.EdgeAgentOnKubernetes,
],
});
const isAllPageSelected = environments.every((env) => selection[env.Id]);
return (
<DialogOverlay
className={styles.dialog}
aria-label="Kubeconfig View"
role="dialog"
>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={onClose}>
×
</button>
<h5 className="modal-title">Download kubeconfig file</h5>
</div>
<div className="modal-body">
<form className="bootbox-form">
<div className="bootbox-prompt-message">
<span>
Select the kubernetes environments to add to the kubeconfig
file. You may select across multiple pages.
</span>
<span className="space-left">
{kubeServiceExpiryQuery.data}
</span>
</div>
</form>
<br />
<Checkbox
id="settings-container-truncate-nae"
label="Select all (in this page)"
checked={isAllPageSelected}
onChange={handleSelectAll}
/>
<div className="datatable">
<div className="bootbox-checkbox-list">
{environments.map((env) => (
<div className={styles.checkbox}>
<Checkbox
id={`${env.Id}`}
label={`${env.Name} (${env.URL})`}
checked={!!selection[env.Id]}
onChange={() =>
toggleSelection(env.Id, !selection[env.Id])
}
/>
</div>
))}
</div>
<div className="footer">
<PaginationControls
showAll={totalCount <= 100}
page={page}
onPageChange={setPage}
pageLimit={pageLimit}
onPageLimitChange={setPageLimit}
totalCount={totalCount}
/>
</div>
</div>
</div>
<div className="modal-footer">
<Button onClick={onClose} color="default">
Cancel
</Button>
<Button onClick={handleDownload}>Download File</Button>
</div>
</div>
</div>
</DialogOverlay>
);
function handleSelectAll() {
environments.forEach((env) => toggleSelection(env.Id, !isAllPageSelected));
}
function handleDownload() {
confirmKubeconfigSelection();
}
async function confirmKubeconfigSelection() {
if (selectionSize === 0) {
notifications.warning('No environment was selected', '');
return;
}
try {
await kcService.downloadKubeconfigFile(
Object.keys(selection).map(Number)
);
onClose();
} catch (e) {
notifications.error('Failed downloading kubeconfig file', e as Error);
}
}
}

View File

@@ -1,27 +0,0 @@
import { useState } from 'react';
import { EnvironmentId } from '@/portainer/environments/types';
export function useSelection() {
const [selection, setSelection] = useState<Record<EnvironmentId, boolean>>(
{}
);
const selectionSize = Object.keys(selection).length;
return { selection, toggle, selectionSize };
function toggle(id: EnvironmentId, selected: boolean) {
setSelection((prevSelection) => {
const newSelection = { ...prevSelection };
if (!selected) {
delete newSelection[id];
} else {
newSelection[id] = true;
}
return newSelection;
});
}
}

View File

@@ -34,6 +34,8 @@ export function PublicSettingsViewModel(settings) {
this.EnableTelemetry = settings.EnableTelemetry;
this.OAuthLogoutURI = settings.OAuthLogoutURI;
this.KubeconfigExpiry = settings.KubeconfigExpiry;
this.Features = settings.Features;
this.Edge = new EdgeSettingsViewModel(settings.Edge);
}
export function InternalAuthSettingsViewModel(data) {
@@ -75,3 +77,10 @@ export function OAuthSettingsViewModel(data) {
this.SSO = data.SSO;
this.LogoutURI = data.LogoutURI;
}
export function EdgeSettingsViewModel(data = {}) {
this.CheckinInterval = data.CheckinInterval;
this.PingInterval = data.PingInterval;
this.SnapshotInterval = data.SnapshotInterval;
this.CommandInterval = data.CommandInterval;
}

View File

@@ -25,6 +25,7 @@ function StateManagerFactory(
UI: {
dismissedInfoPanels: {},
dismissedInfoHash: '',
timesPasswordChangeSkipped: {},
},
};
@@ -49,13 +50,13 @@ function StateManagerFactory(
};
manager.setPasswordChangeSkipped = function (userID) {
state.UI.timesPasswordChangeSkipped = state.UI.timesPasswordChangeSkipped || {};
state.UI.instanceId = state.UI.instanceId || state.application.instanceId;
state.UI.timesPasswordChangeSkipped[userID] = state.UI.timesPasswordChangeSkipped[userID] + 1 || 1;
LocalStorage.storeUIState(state.UI);
};
manager.resetPasswordChangeSkips = function (userID) {
if (state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[userID]) state.UI.timesPasswordChangeSkipped[userID] = 0;
if (state.UI.timesPasswordChangeSkipped[userID]) state.UI.timesPasswordChangeSkipped[userID] = 0;
LocalStorage.storeUIState(state.UI);
};
@@ -141,11 +142,6 @@ function StateManagerFactory(
manager.initialize = initialize;
async function initialize() {
return $async(async () => {
const UIState = LocalStorage.getUIState();
if (UIState) {
state.UI = UIState;
}
const endpointState = LocalStorage.getEndpointState();
if (endpointState) {
state.endpoint = endpointState;
@@ -158,6 +154,16 @@ function StateManagerFactory(
await loadApplicationState();
}
const UIState = LocalStorage.getUIState();
if (UIState) {
state.UI = UIState;
if (state.UI.instanceId && state.UI.instanceId !== state.application.instanceId) {
state.UI.instanceId = state.application.instanceId;
state.UI.timesPasswordChangeSkipped = {};
LocalStorage.storeUIState(state.UI);
}
}
state.loading = false;
$analytics.setPortainerStatus(state.application.instanceId, state.application.version);
$analytics.setOptOut(!state.application.enableTelemetry);

View File

@@ -1,5 +1,6 @@
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
import { react2angular } from '@/react-tools/react2angular';
import { confirm } from '@/portainer/services/modal.service/confirm';
import { SaveAuthSettingsButton } from '../components/SaveAuthSettingsButton';
import { Settings } from '../../types';
@@ -19,6 +20,27 @@ export function InternalAuth({
value,
onChange,
}: Props) {
function onSubmit() {
if (value.RequiredPasswordLength < 10) {
confirm({
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>
@@ -34,7 +56,7 @@ export function InternalAuth({
<div className="form-group">
<PasswordLengthSlider
min={8}
min={1}
max={18}
step={1}
value={value.RequiredPasswordLength}
@@ -42,7 +64,7 @@ export function InternalAuth({
/>
</div>
<SaveAuthSettingsButton onSubmit={onSaveSettings} isLoading={isLoading} />
<SaveAuthSettingsButton onSubmit={onSubmit} isLoading={isLoading} />
</>
);
}

View File

@@ -61,7 +61,7 @@
<portainer-tooltip position="bottom" message="Account that will be used to search for users."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="ldap_username" ng-model="$ctrl.settings.ReaderDN" placeholder="cn=user,dc=domain,dc=tld" />
<input type="text" class="form-control" id="ldap_username" ng-model="$ctrl.settings.ReaderDN" placeholder="{{ $ctrl.clickToSetValues.readerDNPlaceholder }}" />
</div>
</div>

View File

@@ -1,5 +1,5 @@
<ng-form limited-feature-dir="{{::$ctrl.limitedFeatureId}}" limited-feature-class="limited-be" class="ldap-settings-openldap">
<be-feature-indicator feature="$ctrl.limitedFeatureId"></be-feature-indicator>
<be-feature-indicator feature="$ctrl.limitedFeatureId" class="my-8 block"></be-feature-indicator>
<div>
<div class="col-sm-12 form-section-title"> Information </div>
@@ -61,13 +61,12 @@
<!-- Anonymous mode-->
<div class="form-group">
<div class="col-sm-12">
<label for="anonymous_mode" class="control-label text-left">
Anonymous mode
<portainer-tooltip position="bottom" message="Enable this option if the server is configured for Anonymous access."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px">
<label for="anonymous_mode" class="control-label text-left col-sm-3" style="padding-top: 0">
Anonymous mode
<portainer-tooltip position="bottom" message="Enable this option if the server is configured for Anonymous access."></portainer-tooltip>
</label>
<div class="col-sm-9">
<label class="switch">
<input
type="checkbox"
id="anonymous_mode"
@@ -82,11 +81,11 @@
<div ng-if="!$ctrl.settings.AnonymousMode">
<div class="form-group">
<label for="ldap_username" class="col-sm-3 col-lg-2 control-label text-left">
<label for="ldap_username" class="col-sm-3 control-label text-left">
Reader DN
<portainer-tooltip position="bottom" message="Account that will be used to search for users."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<div class="col-sm-9">
<input
type="text"
class="form-control"
@@ -101,7 +100,7 @@
</div>
<div class="form-group">
<label for="ldap_password" class="col-sm-3 col-lg-2 control-label text-left">
<label for="ldap_password" class="col-sm-3 control-label text-left">
Password
<portainer-tooltip position="bottom" message="If you do not enter a password, Portainer will leave the current password unchanged."></portainer-tooltip>
</label>
@@ -121,7 +120,7 @@
</div>
<div class="form-group" ng-if="$ctrl.settings.AnonymousMode">
<label for="ldap_domain_root" class="col-sm-3 col-lg-2 control-label text-left"> Domain root </label>
<label for="ldap_domain_root" class="col-sm-3 control-label text-left"> Domain root </label>
<div class="col-sm-9">
<input
type="text"

View File

@@ -2,6 +2,8 @@ import { useMutation, useQuery, useQueryClient } from 'react-query';
import { notifyError } from '@/portainer/services/notifications';
import { PublicSettingsViewModel } from '../models/settings';
import {
publicSettings,
getSettings,
@@ -9,17 +11,29 @@ import {
} from './settings.service';
import { Settings } from './types';
export function usePublicSettings() {
export function usePublicSettings<T = PublicSettingsViewModel>({
enabled,
select,
}: {
select?: (settings: PublicSettingsViewModel) => T;
enabled?: boolean;
} = {}) {
return useQuery(['settings', 'public'], () => publicSettings(), {
onError: (err) => {
notifyError('Failure', err as Error, 'Unable to retrieve settings');
},
select,
enabled,
});
}
export function useSettings<T = Settings>(select?: (settings: Settings) => T) {
export function useSettings<T = Settings>(
select?: (settings: Settings) => T,
enabled?: boolean
) {
return useQuery(['settings'], getSettings, {
select,
enabled,
meta: {
error: {
title: 'Failure',

View File

@@ -125,4 +125,10 @@ export interface Settings {
AllowStackManagementForRegularUsers: boolean;
AllowDeviceMappingForRegularUsers: boolean;
AllowContainerCapabilitiesForRegularUsers: boolean;
Edge: {
PingInterval: number;
SnapshotInterval: number;
CommandInterval: number;
AsyncMode: boolean;
};
}

View File

@@ -61,7 +61,7 @@ export function CreateAccessToken({
return (
<Widget>
<WidgetBody>
<div>
<div className="form-horizontal">
<FormControl
inputId="input"
label={t('Description')}

View File

@@ -22,7 +22,7 @@ angular.module('portainer.app').controller('AccountController', [
try {
await UserService.updateUserPassword($scope.userID, $scope.formValues.currentPassword, $scope.formValues.newPassword);
Notifications.success('Success', 'Password successfully updated');
StateManager.resetPasswordChangeSkips($scope.userID);
StateManager.resetPasswordChangeSkips($scope.userID.toString());
$scope.forceChangePassword = false;
$state.go('portainer.logout');
} catch (err) {
@@ -34,7 +34,7 @@ angular.module('portainer.app').controller('AccountController', [
$scope.skipPasswordChange = async function () {
try {
if ($scope.userCanSkip()) {
StateManager.setPasswordChangeSkipped($scope.userID);
StateManager.setPasswordChangeSkipped($scope.userID.toString());
$scope.forceChangePassword = false;
$state.go('portainer.home');
}
@@ -48,11 +48,13 @@ angular.module('portainer.app').controller('AccountController', [
};
this.uiCanExit = (newTransition) => {
if ($scope.userRole === 1 && newTransition.to().name === 'portainer.settings.authentication') {
return true;
}
if (newTransition.to().name === 'portainer.logout') {
return true;
if (newTransition) {
if ($scope.userRole === 1 && newTransition.to().name === 'portainer.settings.authentication') {
return true;
}
if (newTransition.to().name === 'portainer.logout') {
return true;
}
}
if ($scope.forceChangePassword) {
ModalService.confirmForceChangePassword();
@@ -130,11 +132,13 @@ angular.module('portainer.app').controller('AccountController', [
$scope.AuthenticationMethod = data.AuthenticationMethod;
if (state.UI.requiredPasswordLength && state.UI.requiredPasswordLength !== data.RequiredPasswordLength) {
StateManager.clearPasswordChangeSkips($scope.userID);
StateManager.clearPasswordChangeSkips();
}
$scope.timesPasswordChangeSkipped =
state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[$scope.userID] ? state.UI.timesPasswordChangeSkipped[$scope.userID] : 0;
state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[$scope.userID.toString()]
? state.UI.timesPasswordChangeSkipped[$scope.userID.toString()]
: 0;
$scope.requiredPasswordLength = data.RequiredPasswordLength;
StateManager.setRequiredPasswordLength(data.RequiredPasswordLength);

View File

@@ -108,6 +108,7 @@
</div>
<custom-templates-variables-definition-field
ng-if="$ctrl.isTemplateVariablesEnabled"
value="$ctrl.formValues.Variables"
on-change="($ctrl.onVariablesChange)"
is-variables-names-from-parent="$ctrl.state.Method === 'editor'"

View File

@@ -2,6 +2,7 @@ import _ from 'lodash';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
class CreateCustomTemplateViewController {
/* @ngInject */
@@ -20,6 +21,8 @@ class CreateCustomTemplateViewController {
StateManager,
});
this.isTemplateVariablesEnabled = isBE;
this.formValues = {
Title: '',
FileContent: '',
@@ -176,6 +179,10 @@ class CreateCustomTemplateViewController {
}
parseTemplate(templateStr) {
if (!this.isTemplateVariablesEnabled) {
return;
}
const variables = getTemplateVariables(templateStr);
const isValid = !!variables;

View File

@@ -18,6 +18,7 @@
>
<advanced-form>
<custom-templates-variables-field
ng-if="$ctrl.isTemplateVariablesEnabled"
definitions="$ctrl.state.selectedTemplate.Variables"
value="$ctrl.formValues.variables"
on-change="($ctrl.onChangeTemplateVariables)"

View File

@@ -2,6 +2,7 @@ import _ from 'lodash-es';
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
class CustomTemplatesViewController {
/* @ngInject */
@@ -34,6 +35,8 @@ class CustomTemplatesViewController {
this.StateManager = StateManager;
this.StackService = StackService;
this.isTemplateVariablesEnabled = isBE;
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
@@ -119,6 +122,10 @@ class CustomTemplatesViewController {
}
renderTemplate() {
if (!this.isTemplateVariablesEnabled) {
return;
}
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
this.onChangeFormValues({ fileContent });
}

View File

@@ -46,6 +46,7 @@
</div>
<custom-templates-variables-definition-field
ng-if="$ctrl.isTemplateVariablesEnabled"
value="$ctrl.formValues.Variables"
on-change="($ctrl.onVariablesChange)"
is-variables-names-from-parent="true"

View File

@@ -3,12 +3,15 @@ import { ResourceControlViewModel } from '@/portainer/access-control/models/Reso
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
class EditCustomTemplateViewController {
/* @ngInject */
constructor($async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
Object.assign(this, { $async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
this.isTemplateVariablesEnabled = isBE;
this.formValues = null;
this.state = {
formValidationError: '',
@@ -127,6 +130,10 @@ class EditCustomTemplateViewController {
}
parseTemplate(templateStr) {
if (!this.isTemplateVariablesEnabled) {
return;
}
const variables = getTemplateVariables(templateStr);
const isValid = !!variables;

View File

@@ -59,6 +59,7 @@
edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }"
commands="state.edgeScriptCommands"
is-nomad-token-visible="state.showNomad"
hide-async-mode="!endpoint.IsEdgeDevice"
></edge-script-form>
<span class="small text-muted">

View File

@@ -70,7 +70,7 @@
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your own template definitions file here. See
<a href="https://docs.portainer.io/advanced/app-templates/build" target="_blank">Portainer documentation</a> for more details.
<a href="https://documentation.portainer.io/v2.0/settings/apps/#build-and-host-your-own-templates" target="_blank">Portainer documentation</a> for more details.
</span>
</div>
<div class="form-group">

View File

@@ -24,7 +24,7 @@
<p class="text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
<span ng-if="external">This stack was created outside of Portainer. Control over this stack is limited.</span>
<span ng-if="orphaned">This stack is orphaned. You can re-associate it with the current environment using the "Associate to this environment" feature.</span>
<span ng-if="orphaned">This stack is orphaned. You can reassociate it with the current environment using the "Associate to this environment" feature.</span>
</p>
</span>
</div>
@@ -90,7 +90,7 @@
<!-- associate -->
<div ng-if="orphaned">
<div class="col-sm-12 form-section-title"> Associate to this environment </div>
<p class="small text-muted"> This feature allows you to re-associate this stack to the current environment. </p>
<p class="small text-muted"> This feature allows you to reassociate this stack to the current environment. </p>
<form class="form-horizontal">
<por-access-control-form form-data="formValues.AccessControlData" hide-title="true"></por-access-control-form>
<div class="form-group">

View File

@@ -2,6 +2,7 @@ import _ from 'lodash';
import { Team } from '@/portainer/teams/types';
import { Role, User, UserId } from '@/portainer/users/types';
import { Environment } from '@/portainer/environments/types';
export function createMockUsers(
count: number,
@@ -59,3 +60,25 @@ export function createMockResourceGroups(subscription: string, count: number) {
return { value: resourceGroups };
}
export function createMockEnvironment(): Environment {
return {
TagIds: [],
GroupId: 1,
Type: 1,
Name: 'environment',
Status: 1,
URL: 'url',
Snapshots: [],
Kubernetes: { Snapshots: [] },
EdgeKey: '',
Id: 3,
UserTrusted: false,
Edge: {
AsyncMode: false,
PingInterval: 0,
CommandInterval: 0,
SnapshotInterval: 0,
},
};
}

View File

@@ -0,0 +1,5 @@
import { ResourceControlResponse } from '@/portainer/access-control/types';
export interface PortainerMetadata {
ResourceControl: ResourceControlResponse;
}

View File

@@ -3,7 +3,6 @@ import { useState } from 'react';
import { CopyButton } from '@/portainer/components/Button/CopyButton';
import { Code } from '@/portainer/components/Code';
import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails';
const deployments = [
@@ -28,10 +27,10 @@ export function DeploymentScripts() {
return null;
}
const { agentVersion } = agentDetailsQuery;
const { agentVersion, agentSecret } = agentDetailsQuery;
const options = deployments.map((c) => {
const code = c.command(agentVersion);
const code = c.command(agentVersion, agentSecret);
return {
id: c.id,
@@ -65,14 +64,41 @@ function DeployCode({ code }: DeployCodeProps) {
);
}
function linuxCommand(agentVersion: string) {
const agentShortVersion = getAgentShortVersion(agentVersion);
function linuxCommand(agentVersion: string, agentSecret: string) {
const secret =
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
return `curl -L https://downloads.portainer.io/ee${agentShortVersion}/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent`;
return `docker network create \\
--driver overlay \\
portainer_agent_network
docker service create \\
--name portainer_agent \\
--network portainer_agent_network \\
-p 9001:9001/tcp ${secret}\\
--mode global \\
--constraint 'node.platform.os == linux' \\
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\
portainer/agent:${agentVersion}
`;
}
function winCommand(agentVersion: string) {
const agentShortVersion = getAgentShortVersion(agentVersion);
function winCommand(agentVersion: string, agentSecret: string) {
const secret =
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
return `curl -L https://downloads.portainer.io/ee${agentShortVersion}/agent-stack-windows.yml -o agent-stack-windows.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent `;
return `docker network create \\
--driver overlay \\
portainer_agent_network && \\
docker service create \\
--name portainer_agent \\
--network portainer_agent_network \\
-p 9001:9001/tcp ${secret}\\
--mode global \\
--constraint 'node.platform.os == windows' \\
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
portainer/agent:${agentVersion}
`;
}

View File

@@ -65,12 +65,7 @@ export function WizardEndpointsList({ environmentIds }: Props) {
</div>
{isEdgeEnvironment(environment.Type) && (
<div className={styles.wizardListEdgeStatus}>
<EdgeIndicator
edgeId={environment.EdgeID}
checkInInterval={environment.EdgeCheckinInterval}
queryDate={environment.QueryDate}
lastCheckInDate={environment.LastCheckInDate}
/>
<EdgeIndicator environment={environment} />
</div>
)}
</div>

View File

@@ -11,6 +11,9 @@ ARCH=$2
DOCKER_VERSION=${3:1}
DOWNLOAD_FOLDER=".tmp/download"
if [[ ${PLATFORM} == "darwin" ]]; then
PLATFORM="mac"
fi
if [[ ${ARCH} == "amd64" ]]; then
ARCH="x86_64"
@@ -18,6 +21,10 @@ elif [[ ${ARCH} == "arm" ]]; then
ARCH="armhf"
elif [[ ${ARCH} == "arm64" ]]; then
ARCH="aarch64"
elif [[ ${ARCH} == "ppc64le" ]]; then
DOCKER_VERSION="18.06.3-ce"
elif [[ ${ARCH} == "s390x" ]]; then
DOCKER_VERSION="18.06.3-ce"
fi
rm -rf "${DOWNLOAD_FOLDER}"

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.15.0",
"version": "2.14.0",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -70,7 +70,6 @@
"@lineup-lite/hooks": "^1.6.0",
"@nxmix/tokenize-ansi": "^3.0.0",
"@open-amt-cloud-toolkit/ui-toolkit-react": "2.0.0",
"@reach/dialog": "^0.17.0",
"@reach/menu-button": "^0.16.1",
"@uirouter/angularjs": "1.0.11",
"@uirouter/react": "^1.0.7",

115
yarn.lock
View File

@@ -1158,13 +1158,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.13":
version "7.18.3"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.3.tgz#c7b654b57f6f63cf7f8b418ac9ca04408c4579f4"
integrity sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
@@ -1946,18 +1939,6 @@
"@reach/utils" "0.16.0"
tslib "^2.3.0"
"@reach/dialog@^0.17.0":
version "0.17.0"
resolved "https://registry.npmmirror.com/@reach/dialog/-/dialog-0.17.0.tgz#81c48dd4405945dfc6b6c3e5e125db2c4324e9e8"
integrity sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==
dependencies:
"@reach/portal" "0.17.0"
"@reach/utils" "0.17.0"
prop-types "^15.7.2"
react-focus-lock "^2.5.2"
react-remove-scroll "^2.4.3"
tslib "^2.3.0"
"@reach/dropdown@0.16.2":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.16.2.tgz#4aa7df0f716cb448d01bc020d54df595303d5fa6"
@@ -2006,15 +1987,6 @@
tiny-warning "^1.0.3"
tslib "^2.3.0"
"@reach/portal@0.17.0":
version "0.17.0"
resolved "https://registry.npmmirror.com/@reach/portal/-/portal-0.17.0.tgz#1dd69ffc8ffc8ba3e26dd127bf1cc4b15f0c6bdc"
integrity sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==
dependencies:
"@reach/utils" "0.17.0"
tiny-warning "^1.0.3"
tslib "^2.3.0"
"@reach/rect@0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.16.0.tgz#78cf6acefe2e83d3957fa84f938f6e1fc5700f16"
@@ -2034,14 +2006,6 @@
tiny-warning "^1.0.3"
tslib "^2.3.0"
"@reach/utils@0.17.0":
version "0.17.0"
resolved "https://registry.npmmirror.com/@reach/utils/-/utils-0.17.0.tgz#3d1d2ec56d857f04fe092710d8faee2b2b121303"
integrity sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==
dependencies:
tiny-warning "^1.0.3"
tslib "^2.3.0"
"@sgratzl/boxplots@^1.2.2":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@sgratzl/boxplots/-/boxplots-1.3.0.tgz#c9063d98e33a15f880cf4bd3531be71497e2a94e"
@@ -7073,11 +7037,6 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==
detect-node@^2.0.4, detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
@@ -8654,13 +8613,6 @@ fn.name@1.x.x:
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
focus-lock@^0.11.2:
version "0.11.2"
resolved "https://registry.npmmirror.com/focus-lock/-/focus-lock-0.11.2.tgz#aeef3caf1cea757797ac8afdebaec8fd9ab243ed"
integrity sha512-pZ2bO++NWLHhiKkgP1bEXHhR1/OjVcSvlCJ98aNJDFeb7H5OOQaO+SKOZle6041O9rv2tmbrO4JzClAvDUHf0g==
dependencies:
tslib "^2.0.3"
follow-redirects@^1.0.0, follow-redirects@^1.14.4:
version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
@@ -8943,11 +8895,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
has "^1.0.3"
has-symbols "^1.0.1"
get-nonce@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
get-package-type@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
@@ -14266,13 +14213,6 @@ rc-util@^5.16.1, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0:
react-is "^16.12.0"
shallowequal "^1.1.0"
react-clientside-effect@^1.2.6:
version "1.2.6"
resolved "https://registry.npmmirror.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
integrity sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==
dependencies:
"@babel/runtime" "^7.12.13"
react-colorful@^5.1.2:
version "5.5.1"
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
@@ -14342,18 +14282,6 @@ react-feather@^2.0.9:
dependencies:
prop-types "^15.7.2"
react-focus-lock@^2.5.2:
version "2.9.1"
resolved "https://registry.npmmirror.com/react-focus-lock/-/react-focus-lock-2.9.1.tgz#094cfc19b4f334122c73bb0bff65d77a0c92dd16"
integrity sha512-pSWOQrUmiKLkffPO6BpMXN7SNKXMsuOakl652IBuALAu1esk+IcpJyM+ALcYzPTTFz1rD0R54aB9A4HuP5t1Wg==
dependencies:
"@babel/runtime" "^7.0.0"
focus-lock "^0.11.2"
prop-types "^15.6.2"
react-clientside-effect "^1.2.6"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-helmet-async@^1.0.7:
version "1.2.2"
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.2.2.tgz#38d58d32ebffbc01ba42b5ad9142f85722492389"
@@ -14424,25 +14352,6 @@ react-refresh@^0.11.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
react-remove-scroll-bar@^2.3.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.1.tgz#9f13b05b249eaa57c8d646c1ebb83006b3581f5f"
integrity sha512-IvGX3mJclEF7+hga8APZczve1UyGMkMG+tjS0o/U1iLgvZRpjFAQEUBJ4JETfvbNlfNnZnoDyWJCICkA15Mghg==
dependencies:
react-style-singleton "^2.2.0"
tslib "^2.0.0"
react-remove-scroll@^2.4.3:
version "2.5.3"
resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.3.tgz#a152196e710e8e5811be39dc352fd8a90b05c961"
integrity sha512-NQ1bXrxKrnK5pFo/GhLkXeo3CrK5steI+5L+jynwwIemvZyfXqaL0L5BzwJd7CSwNCU723DZaccvjuyOdoy3Xw==
dependencies:
react-remove-scroll-bar "^2.3.1"
react-style-singleton "^2.2.0"
tslib "^2.0.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-router-dom@^6.0.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec"
@@ -14489,15 +14398,6 @@ react-sizeme@^3.0.1:
shallowequal "^1.1.0"
throttle-debounce "^3.0.1"
react-style-singleton@^2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.0.tgz#70f45f5fef97fdb9a52eed98d1839fa6b9032b22"
integrity sha512-nK7mN92DMYZEu3cQcAhfwE48NpzO5RpxjG4okbSqRRbfal9Pk+fG2RdQXTMp+f6all1hB9LIJSt+j7dCYrU11g==
dependencies:
get-nonce "^1.0.0"
invariant "^2.2.4"
tslib "^2.0.0"
react-syntax-highlighter@^13.5.3:
version "13.5.3"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz#9712850f883a3e19eb858cf93fad7bb357eea9c6"
@@ -17044,13 +16944,6 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
use-callback-ref@^1.3.0:
version "1.3.0"
resolved "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5"
integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==
dependencies:
tslib "^2.0.0"
use-composed-ref@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.2.1.tgz#9bdcb5ccd894289105da2325e1210079f56bf849"
@@ -17068,14 +16961,6 @@ use-latest@^1.0.0:
dependencies:
use-isomorphic-layout-effect "^1.0.0"
use-sidecar@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"
integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==
dependencies:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"