Compare commits
24 Commits
fix/EE-332
...
2.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b998d270 | ||
|
|
605ff8c1da | ||
|
|
13f93f4262 | ||
|
|
16be5ed329 | ||
|
|
c6612898f3 | ||
|
|
564f34b0ba | ||
|
|
392fbdb4a7 | ||
|
|
a826c78786 | ||
|
|
a35f0607f1 | ||
|
|
081d32af0d | ||
|
|
4cc0b1f567 | ||
|
|
d4da7e1760 | ||
|
|
aced418880 | ||
|
|
614f42fe5a | ||
|
|
58736fe93b | ||
|
|
b78330b10d | ||
|
|
eed4a92ca8 | ||
|
|
0e7468a1e8 | ||
|
|
b807481f1c | ||
|
|
da27de2154 | ||
|
|
6743e4fbb2 | ||
|
|
b489ffaa63 | ||
|
|
6e12499d61 | ||
|
|
f7acbe16ba |
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.15.0
|
||||
// @version 2.14.0
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,7 +23,7 @@ type (
|
||||
}
|
||||
|
||||
portainerRegistryAuthenticationHeader struct {
|
||||
RegistryId portainer.RegistryID `json:"registryId"`
|
||||
RegistryId *portainer.RegistryID `json:"registryId"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.checkbox {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -125,4 +125,10 @@ export interface Settings {
|
||||
AllowStackManagementForRegularUsers: boolean;
|
||||
AllowDeviceMappingForRegularUsers: boolean;
|
||||
AllowContainerCapabilitiesForRegularUsers: boolean;
|
||||
Edge: {
|
||||
PingInterval: number;
|
||||
SnapshotInterval: number;
|
||||
CommandInterval: number;
|
||||
AsyncMode: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export function CreateAccessToken({
|
||||
return (
|
||||
<Widget>
|
||||
<WidgetBody>
|
||||
<div>
|
||||
<div className="form-horizontal">
|
||||
<FormControl
|
||||
inputId="input"
|
||||
label={t('Description')}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
5
app/react/docker/types.ts
Normal file
5
app/react/docker/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ResourceControlResponse } from '@/portainer/access-control/types';
|
||||
|
||||
export interface PortainerMetadata {
|
||||
ResourceControl: ResourceControlResponse;
|
||||
}
|
||||
@@ -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}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
115
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user