mirror of https://github.com/portainer/portainer
feat(edge): show correct heartbeat and sync aeec changes [EE-2876] (#6769)
parent
76d1b70644
commit
e217ac7121
|
@ -587,7 +587,6 @@
|
|||
"AllowVolumeBrowserForRegularUsers": false,
|
||||
"AuthenticationMethod": 1,
|
||||
"BlackListedLabels": [],
|
||||
"DisableTrustOnFirstConnect": false,
|
||||
"DisplayDonationHeader": false,
|
||||
"DisplayExternalContributors": false,
|
||||
"EdgeAgentCheckinInterval": 5,
|
||||
|
@ -642,6 +641,7 @@
|
|||
},
|
||||
"SnapshotInterval": "5m",
|
||||
"TemplatesURL": "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json",
|
||||
"TrustOnFirstConnect": false,
|
||||
"UserSessionTimeout": "8h",
|
||||
"fdoConfiguration": {
|
||||
"enabled": false,
|
||||
|
|
|
@ -326,6 +326,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
|||
EdgeCheckinInterval: payload.EdgeCheckinInterval,
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
IsEdgeDevice: payload.IsEdgeDevice,
|
||||
UserTrusted: true,
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
|
|
@ -14,6 +14,12 @@ import (
|
|||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceFilterAll = "all"
|
||||
EdgeDeviceFilterTrusted = "trusted"
|
||||
EdgeDeviceFilterUntrusted = "untrusted"
|
||||
)
|
||||
|
||||
// @id EndpointList
|
||||
// @summary List environments(endpoints)
|
||||
// @description List all environments(endpoints) based on the current user authorizations. Will
|
||||
|
@ -32,6 +38,7 @@ import (
|
|||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param edgeDeviceFilter query string false "will return only these edge devices" Enum("all", "trusted", "untrusted")
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
// @router /endpoints [get]
|
||||
|
@ -91,8 +98,8 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
|||
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
|
||||
}
|
||||
|
||||
edgeDeviceFilter, edgeDeviceFilterErr := request.RetrieveBooleanQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilterErr == nil {
|
||||
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilter != "" {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
|
||||
}
|
||||
|
||||
|
@ -240,17 +247,38 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int)
|
|||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter bool) []portainer.Endpoint {
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
if edgeDeviceFilter != EdgeDeviceFilterAll && edgeDeviceFilter != EdgeDeviceFilterTrusted && edgeDeviceFilter != EdgeDeviceFilterUntrusted {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if edgeDeviceFilter == endpoint.IsEdgeDevice {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
|
||||
if !endpoint.IsEdgeDevice {
|
||||
return false
|
||||
}
|
||||
|
||||
switch edgeDeviceFilter {
|
||||
case EdgeDeviceFilterAll:
|
||||
return true
|
||||
case EdgeDeviceFilterTrusted:
|
||||
return endpoint.UserTrusted
|
||||
case EdgeDeviceFilterUntrusted:
|
||||
return !endpoint.UserTrusted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
expected []portainer.EndpointID
|
||||
filter string
|
||||
}
|
||||
|
||||
func Test_endpointList(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1}
|
||||
err := store.Endpoint().Create(&trustedEndpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
|
||||
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1}
|
||||
err = store.Endpoint().Create(&untrustedEndpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
|
||||
regularEndpoint := portainer.Endpoint{ID: 3, IsEdgeDevice: false, GroupID: 1}
|
||||
err = store.Endpoint().Create(®ularEndpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
|
||||
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
tests := []endpointListEdgeDeviceTest{
|
||||
{
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID},
|
||||
EdgeDeviceFilterAll,
|
||||
},
|
||||
{
|
||||
[]portainer.EndpointID{trustedEndpoint.ID},
|
||||
EdgeDeviceFilterTrusted,
|
||||
},
|
||||
{
|
||||
[]portainer.EndpointID{untrustedEndpoint.ID},
|
||||
EdgeDeviceFilterUntrusted,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
req := buildEndpointListRequest(test.filter)
|
||||
resp, err := doEndpointListRequest(req, h, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range resp {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.Equal(test.expected, respIds, "response should contain all edge devices")
|
||||
}
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(filter string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
|
||||
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
restrictedCtx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
|
||||
req = req.WithContext(restrictedCtx)
|
||||
|
||||
req.Header.Add("Authorization", "Bearer dummytoken")
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func doEndpointListRequest(req *http.Request, h *Handler, is *assert.Assertions) ([]portainer.Endpoint, error) {
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, req)
|
||||
|
||||
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
|
||||
body, err := io.ReadAll(rr.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := []portainer.Endpoint{}
|
||||
err = json.Unmarshal(body, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
|
@ -46,8 +46,6 @@ type endpointUpdatePayload struct {
|
|||
EdgeCheckinInterval *int `example:"5"`
|
||||
// Associated Kubernetes data
|
||||
Kubernetes *portainer.KubernetesData
|
||||
// Whether the device has been trusted or not by the user
|
||||
UserTrusted *bool
|
||||
}
|
||||
|
||||
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||
|
@ -273,10 +271,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
}
|
||||
|
||||
if payload.UserTrusted != nil {
|
||||
endpoint.UserTrusted = *payload.UserTrusted
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
|
@ -21,10 +20,21 @@ func hideFields(endpoint *portainer.Endpoint) {
|
|||
}
|
||||
}
|
||||
|
||||
// This requestBouncer exists because security.RequestBounder is a type and not an interface.
|
||||
// Therefore we can not swit it out with a dummy bouncer for go tests. This interface works around it
|
||||
type requestBouncer interface {
|
||||
AuthenticatedAccess(h http.Handler) http.Handler
|
||||
AdminAccess(h http.Handler) http.Handler
|
||||
RestrictedAccess(h http.Handler) http.Handler
|
||||
PublicAccess(h http.Handler) http.Handler
|
||||
AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error
|
||||
AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle environment(endpoint) operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
requestBouncer requestBouncer
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
|
@ -38,7 +48,7 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer requestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
|
|
|
@ -43,8 +43,8 @@ type settingsUpdatePayload struct {
|
|||
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
|
||||
// Kubectl Shell Image
|
||||
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
|
||||
// DisableTrustOnFirstConnect makes Portainer require explicit user trust of the edge agent before accepting the connection
|
||||
DisableTrustOnFirstConnect *bool `example:"false"`
|
||||
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
|
||||
TrustOnFirstConnect *bool `example:"false"`
|
||||
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
|
||||
EnforceEdgeID *bool `example:"false"`
|
||||
// EdgePortainerURL is the URL that is exposed to edge agents
|
||||
|
@ -180,8 +180,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
||||
}
|
||||
|
||||
if payload.DisableTrustOnFirstConnect != nil {
|
||||
settings.DisableTrustOnFirstConnect = *payload.DisableTrustOnFirstConnect
|
||||
if payload.TrustOnFirstConnect != nil {
|
||||
settings.TrustOnFirstConnect = *payload.TrustOnFirstConnect
|
||||
}
|
||||
|
||||
if payload.EnforceEdgeID != nil {
|
||||
|
|
|
@ -144,7 +144,7 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
|||
return fmt.Errorf("could not retrieve the settings: %w", err)
|
||||
}
|
||||
|
||||
if settings.DisableTrustOnFirstConnect {
|
||||
if !settings.TrustOnFirstConnect {
|
||||
return errors.New("the device has not been trusted yet")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type composeStackManager struct{}
|
||||
|
||||
func NewComposeStackManager() *composeStackManager {
|
||||
return &composeStackManager{}
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) ComposeSyntaxMaxVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) NormalizeStackName(name string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) Down(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *composeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
package testhelpers
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type testRequestBouncer struct {
|
||||
}
|
||||
|
@ -13,3 +17,23 @@ func NewTestRequestBouncer() *testRequestBouncer {
|
|||
func (testRequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (testRequestBouncer) AdminAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (testRequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (testRequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (testRequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testRequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -809,8 +809,8 @@ type (
|
|||
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
|
||||
// KubectlImage, defaults to portainer/kubectl-shell
|
||||
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
|
||||
// DisableTrustOnFirstConnect makes Portainer require explicit user trust of the edge agent before accepting the connection
|
||||
DisableTrustOnFirstConnect bool `json:"DisableTrustOnFirstConnect" example:"false"`
|
||||
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
|
||||
TrustOnFirstConnect bool `json:"TrustOnFirstConnect" example:"false"`
|
||||
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
|
||||
EnforceEdgeID bool `json:"EnforceEdgeID" example:"false"`
|
||||
// Container environment parameter AGENT_SECRET
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SettableQuickActionsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/portainer/components/datatables/types';
|
||||
|
||||
export type DockerContainerStatus =
|
||||
| 'paused'
|
||||
|
@ -13,13 +20,13 @@ export type DockerContainerStatus =
|
|||
|
||||
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
||||
|
||||
export interface ContainersTableSettings {
|
||||
hiddenQuickActions: QuickAction[];
|
||||
hiddenColumns: string[];
|
||||
export interface ContainersTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SettableQuickActionsTableSettings<QuickAction>,
|
||||
RefreshableTableSettings {
|
||||
truncateContainerName: number;
|
||||
autoRefreshRate: number;
|
||||
pageSize: number;
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
|
||||
export interface Port {
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
import {
|
||||
Column,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
|
||||
import { Button } from '@/portainer/components/Button';
|
||||
import { Table } from '@/portainer/components/datatables/components';
|
||||
import {
|
||||
SearchBar,
|
||||
useSearchBarState,
|
||||
} from '@/portainer/components/datatables/components/SearchBar';
|
||||
import { SelectedRowsCount } from '@/portainer/components/datatables/components/SelectedRowsCount';
|
||||
import { PaginationControls } from '@/portainer/components/pagination-controls';
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { useTableSettings } from '@/portainer/components/datatables/components/useTableSettings';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { useAssociateDeviceMutation } from '../queries';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
const columns: readonly Column<Environment>[] = [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: (row) => row.Name,
|
||||
id: 'name',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
Header: 'Edge ID',
|
||||
accessor: (row) => row.EdgeID,
|
||||
id: 'edge-id',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface Props {
|
||||
devices: Environment[];
|
||||
isLoading: boolean;
|
||||
totalCount: number;
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
devices,
|
||||
storageKey,
|
||||
isLoading,
|
||||
totalCount,
|
||||
}: Props) {
|
||||
const associateMutation = useAssociateDeviceMutation();
|
||||
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||
const { settings, setTableSettings } = useTableSettings<TableSettings>();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Environment>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: devices,
|
||||
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
isRowSelectable() {
|
||||
return true;
|
||||
},
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(originalRow: Environment) {
|
||||
return originalRow.Id.toString();
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Table.Container>
|
||||
<Table.Title label="Edge Devices Waiting Room" icon="" />
|
||||
<Table.Actions>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleAssociateDevice(selectedFlatRows.map((r) => r.original))
|
||||
}
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
>
|
||||
Associate Device
|
||||
</Button>
|
||||
</Table.Actions>
|
||||
|
||||
<SearchBar onChange={handleSearchBarChange} value={searchBarValue} />
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<Environment>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
emptyContent="No Edge Devices found"
|
||||
prepareRow={prepareRow}
|
||||
rows={page}
|
||||
isLoading={isLoading}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Table.Footer>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={handlePageLimitChange}
|
||||
/>
|
||||
</Table.Footer>
|
||||
</Table.Container>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function handleSortChange(colId: string, desc: boolean) {
|
||||
setTableSettings({ sortBy: { id: colId, desc } });
|
||||
}
|
||||
|
||||
function handlePageLimitChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings({ pageSize });
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setGlobalFilter(value);
|
||||
setSearchBarValue(value);
|
||||
}
|
||||
|
||||
function handleAssociateDevice(devices: Environment[]) {
|
||||
associateMutation.mutate(
|
||||
devices.map((d) => d.Id),
|
||||
{
|
||||
onSuccess() {
|
||||
notifySuccess('Edge devices associated successfully');
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/portainer/components/datatables/types';
|
||||
|
||||
export interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
|
@ -0,0 +1,47 @@
|
|||
import { useRouter } from '@uirouter/react';
|
||||
|
||||
import { TableSettingsProvider } from '@/portainer/components/datatables/components/useTableSettings';
|
||||
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||
import { useEnvironmentList } from '@/portainer/environments/queries';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
import { DataTable } from './Datatable/Datatable';
|
||||
import { TableSettings } from './Datatable/types';
|
||||
|
||||
export function WaitingRoomView() {
|
||||
const storageKey = 'edge-devices-waiting-room';
|
||||
const router = useRouter();
|
||||
const { environments, isLoading, totalCount } = useEnvironmentList({
|
||||
edgeDeviceFilter: 'untrusted',
|
||||
});
|
||||
|
||||
if (process.env.PORTAINER_EDITION !== 'BE') {
|
||||
router.stateService.go('edge.devices');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Waiting Room"
|
||||
breadcrumbs={[
|
||||
{ label: 'Edge Devices', link: 'edge.devices' },
|
||||
{ label: 'Waiting Room' },
|
||||
]}
|
||||
/>
|
||||
<TableSettingsProvider<TableSettings>
|
||||
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
|
||||
storageKey={storageKey}
|
||||
>
|
||||
<DataTable
|
||||
devices={environments}
|
||||
totalCount={totalCount}
|
||||
isLoading={isLoading}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const WaitingRoomViewAngular = r2a(WaitingRoomView, []);
|
|
@ -0,0 +1 @@
|
|||
export { WaitingRoomView, WaitingRoomViewAngular } from './WaitingRoomView';
|
|
@ -0,0 +1,33 @@
|
|||
import { useMutation, useQueryClient } from 'react-query';
|
||||
|
||||
import { EnvironmentId } from '@/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
|
||||
export function useAssociateDeviceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(
|
||||
(ids: EnvironmentId[]) =>
|
||||
promiseSequence(ids.map((id) => () => associateDevice(id))),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['environments']);
|
||||
},
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Failed to associate devices',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function associateDevice(environmentId: EnvironmentId) {
|
||||
try {
|
||||
await axios.post(`/endpoints/${environmentId}/edge/trust`);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Failed to associate device');
|
||||
}
|
||||
}
|
|
@ -3,134 +3,150 @@ import angular from 'angular';
|
|||
import edgeStackModule from './views/edge-stacks';
|
||||
import edgeDevicesModule from './devices';
|
||||
import { componentsModule } from './components';
|
||||
import { WaitingRoomViewAngular } from './EdgeDevices/WaitingRoomView';
|
||||
|
||||
angular.module('portainer.edge', [edgeStackModule, edgeDevicesModule, componentsModule]).config(function config($stateRegistryProvider) {
|
||||
const edge = {
|
||||
name: 'edge',
|
||||
url: '/edge',
|
||||
parent: 'root',
|
||||
abstract: true,
|
||||
};
|
||||
angular
|
||||
.module('portainer.edge', [edgeStackModule, edgeDevicesModule, componentsModule])
|
||||
.component('waitingRoomView', WaitingRoomViewAngular)
|
||||
.config(function config($stateRegistryProvider) {
|
||||
const edge = {
|
||||
name: 'edge',
|
||||
url: '/edge',
|
||||
parent: 'root',
|
||||
abstract: true,
|
||||
};
|
||||
|
||||
const groups = {
|
||||
name: 'edge.groups',
|
||||
url: '/groups',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeGroupsView',
|
||||
const groups = {
|
||||
name: 'edge.groups',
|
||||
url: '/groups',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeGroupsView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const groupsNew = {
|
||||
name: 'edge.groups.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeGroupView',
|
||||
const groupsNew = {
|
||||
name: 'edge.groups.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeGroupView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const groupsEdit = {
|
||||
name: 'edge.groups.edit',
|
||||
url: '/:groupId',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'editEdgeGroupView',
|
||||
const groupsEdit = {
|
||||
name: 'edge.groups.edit',
|
||||
url: '/:groupId',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'editEdgeGroupView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const stacks = {
|
||||
name: 'edge.stacks',
|
||||
url: '/stacks',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeStacksView',
|
||||
const stacks = {
|
||||
name: 'edge.stacks',
|
||||
url: '/stacks',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeStacksView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const stacksNew = {
|
||||
name: 'edge.stacks.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeStackView',
|
||||
const stacksNew = {
|
||||
name: 'edge.stacks.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeStackView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const stacksEdit = {
|
||||
name: 'edge.stacks.edit',
|
||||
url: '/:stackId',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'editEdgeStackView',
|
||||
const stacksEdit = {
|
||||
name: 'edge.stacks.edit',
|
||||
url: '/:stackId',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'editEdgeStackView',
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
tab: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const edgeJobs = {
|
||||
name: 'edge.jobs',
|
||||
url: '/jobs',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobsView',
|
||||
params: {
|
||||
tab: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const edgeJob = {
|
||||
name: 'edge.jobs.job',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobView',
|
||||
const edgeJobs = {
|
||||
name: 'edge.jobs',
|
||||
url: '/jobs',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobsView',
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
tab: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const edgeJobCreation = {
|
||||
name: 'edge.jobs.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeJobView',
|
||||
const edgeJob = {
|
||||
name: 'edge.jobs.job',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobView',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const edgeDevices = {
|
||||
name: 'edge.devices',
|
||||
url: '/devices',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeDevicesView',
|
||||
params: {
|
||||
tab: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(edge);
|
||||
const edgeJobCreation = {
|
||||
name: 'edge.jobs.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeJobView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(groupsNew);
|
||||
$stateRegistryProvider.register(groupsEdit);
|
||||
const edgeDevices = {
|
||||
name: 'edge.devices',
|
||||
url: '/devices',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeDevicesView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(stacks);
|
||||
$stateRegistryProvider.register(stacksNew);
|
||||
$stateRegistryProvider.register(stacksEdit);
|
||||
if (process.env.PORTAINER_EDITION === 'BE') {
|
||||
$stateRegistryProvider.register({
|
||||
name: 'edge.devices.waiting-room',
|
||||
url: '/waiting-room',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'waitingRoomView',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$stateRegistryProvider.register(edgeJobs);
|
||||
$stateRegistryProvider.register(edgeJob);
|
||||
$stateRegistryProvider.register(edgeJobCreation);
|
||||
$stateRegistryProvider.register(edge);
|
||||
|
||||
$stateRegistryProvider.register(edgeDevices);
|
||||
});
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(groupsNew);
|
||||
$stateRegistryProvider.register(groupsEdit);
|
||||
|
||||
$stateRegistryProvider.register(stacks);
|
||||
$stateRegistryProvider.register(stacksNew);
|
||||
$stateRegistryProvider.register(stacksEdit);
|
||||
|
||||
$stateRegistryProvider.register(edgeJobs);
|
||||
$stateRegistryProvider.register(edgeJob);
|
||||
$stateRegistryProvider.register(edgeJobCreation);
|
||||
|
||||
$stateRegistryProvider.register(edgeDevices);
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ export function EdgePropertiesForm({
|
|||
}: Props) {
|
||||
return (
|
||||
<form className="form-horizontal">
|
||||
<FormSectionTitle>Edge script settings</FormSectionTitle>
|
||||
<FormSectionTitle>Edge agent deployment script</FormSectionTitle>
|
||||
|
||||
<OsSelector
|
||||
value={values.os}
|
||||
|
|
|
@ -50,7 +50,7 @@ export interface EdgeDevicesTableProps {
|
|||
isEnabled: boolean;
|
||||
isFdoEnabled: boolean;
|
||||
isOpenAmtEnabled: boolean;
|
||||
disableTrustOnFirstConnect: boolean;
|
||||
showWaitingRoomLink: boolean;
|
||||
mpsServer: string;
|
||||
dataset: Environment[];
|
||||
groups: EnvironmentGroup[];
|
||||
|
@ -62,7 +62,7 @@ export function EdgeDevicesDatatable({
|
|||
storageKey,
|
||||
isFdoEnabled,
|
||||
isOpenAmtEnabled,
|
||||
disableTrustOnFirstConnect,
|
||||
showWaitingRoomLink,
|
||||
mpsServer,
|
||||
dataset,
|
||||
groups,
|
||||
|
@ -164,6 +164,7 @@ export function EdgeDevicesDatatable({
|
|||
isFDOEnabled={isFdoEnabled}
|
||||
isOpenAMTEnabled={isOpenAmtEnabled}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
showWaitingRoomLink={showWaitingRoomLink}
|
||||
/>
|
||||
</TableActions>
|
||||
|
||||
|
@ -216,7 +217,6 @@ export function EdgeDevicesDatatable({
|
|||
return (
|
||||
<RowProvider
|
||||
key={key}
|
||||
disableTrustOnFirstConnect={disableTrustOnFirstConnect}
|
||||
isOpenAmtEnabled={isOpenAmtEnabled}
|
||||
groupName={group[0]?.Name}
|
||||
>
|
||||
|
|
|
@ -7,12 +7,14 @@ import { promptAsync } from '@/portainer/services/modal.service/prompt';
|
|||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { activateDevice } from '@/portainer/hostmanagement/open-amt/open-amt.service';
|
||||
import { deleteEndpoint } from '@/portainer/environments/environment.service';
|
||||
import { Link } from '@/portainer/components/Link';
|
||||
|
||||
interface Props {
|
||||
selectedItems: Environment[];
|
||||
isFDOEnabled: boolean;
|
||||
isOpenAMTEnabled: boolean;
|
||||
setLoadingMessage(message: string): void;
|
||||
showWaitingRoomLink: boolean;
|
||||
}
|
||||
|
||||
export function EdgeDevicesDatatableActions({
|
||||
|
@ -20,6 +22,7 @@ export function EdgeDevicesDatatableActions({
|
|||
isOpenAMTEnabled,
|
||||
isFDOEnabled,
|
||||
setLoadingMessage,
|
||||
showWaitingRoomLink,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
|
@ -48,6 +51,12 @@ export function EdgeDevicesDatatableActions({
|
|||
Associate with OpenAMT
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showWaitingRoomLink && (
|
||||
<Link to="edge.devices.waiting-room">
|
||||
<Button>Waiting Room</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ export const EdgeDevicesDatatableAngular = react2angular(
|
|||
'onRefresh',
|
||||
'setLoadingMessage',
|
||||
'isFdoEnabled',
|
||||
'disableTrustOnFirstConnect',
|
||||
'showWaitingRoomLink',
|
||||
'isOpenAmtEnabled',
|
||||
'mpsServer',
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createContext, useContext, useMemo, PropsWithChildren } from 'react';
|
||||
|
||||
interface RowContextState {
|
||||
disableTrustOnFirstConnect: boolean;
|
||||
isOpenAmtEnabled: boolean;
|
||||
groupName?: string;
|
||||
}
|
||||
|
@ -9,20 +8,18 @@ interface RowContextState {
|
|||
const RowContext = createContext<RowContextState | null>(null);
|
||||
|
||||
export interface RowProviderProps {
|
||||
disableTrustOnFirstConnect: boolean;
|
||||
groupName?: string;
|
||||
isOpenAmtEnabled: boolean;
|
||||
}
|
||||
|
||||
export function RowProvider({
|
||||
disableTrustOnFirstConnect,
|
||||
groupName,
|
||||
isOpenAmtEnabled,
|
||||
children,
|
||||
}: PropsWithChildren<RowProviderProps>) {
|
||||
const state = useMemo(
|
||||
() => ({ disableTrustOnFirstConnect, groupName, isOpenAmtEnabled }),
|
||||
[disableTrustOnFirstConnect, groupName, isOpenAmtEnabled]
|
||||
() => ({ groupName, isOpenAmtEnabled }),
|
||||
[groupName, isOpenAmtEnabled]
|
||||
);
|
||||
|
||||
return <RowContext.Provider value={state}>{children}</RowContext.Provider>;
|
||||
|
|
|
@ -4,14 +4,9 @@ import { useRouter, useSref } from '@uirouter/react';
|
|||
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { ActionsMenu } from '@/portainer/components/datatables/components/ActionsMenu';
|
||||
import {
|
||||
snapshotEndpoint,
|
||||
trustEndpoint,
|
||||
} from '@/portainer/environments/environment.service';
|
||||
import { snapshotEndpoint } from '@/portainer/environments/environment.service';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { getRoute } from '@/portainer/environments/utils';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
import { useRowContext } from '@/edge/devices/components/EdgeDevicesDatatable/columns/RowContext';
|
||||
|
||||
export const actions: Column<Environment> = {
|
||||
Header: 'Actions',
|
||||
|
@ -39,8 +34,6 @@ export function ActionsCell({
|
|||
|
||||
const showRefreshSnapshot = false; // remove and show MenuItem when feature is available
|
||||
|
||||
const { disableTrustOnFirstConnect } = useRowContext();
|
||||
|
||||
return (
|
||||
<ActionsMenu>
|
||||
<MenuLink href={browseLinkProps.href} onClick={browseLinkProps.onClick}>
|
||||
|
@ -51,9 +44,6 @@ export function ActionsCell({
|
|||
Refresh Snapshot
|
||||
</MenuItem>
|
||||
)}
|
||||
{disableTrustOnFirstConnect && !environment.UserTrusted && (
|
||||
<MenuLink onClick={trustDevice}>Trust</MenuLink>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
);
|
||||
|
||||
|
@ -71,37 +61,4 @@ export function ActionsCell({
|
|||
await router.stateService.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function trustDevice() {
|
||||
const confirmed = await confirmAsync({
|
||||
title: '',
|
||||
message: `Mark ${environment.Name} as trusted?`,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Trust',
|
||||
className: 'btn-primary',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await trustEndpoint(environment.Id);
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'An error occurred while trusting the environment'
|
||||
);
|
||||
} finally {
|
||||
await router.stateService.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Environment, EnvironmentStatus } from '@/portainer/environments/types';
|
||||
import { useRowContext } from '@/edge/devices/components/EdgeDevicesDatatable/columns/RowContext';
|
||||
import { Environment } from '@/portainer/environments/types';
|
||||
import { EdgeIndicator } from '@/portainer/home/EnvironmentList/EnvironmentItem/EdgeIndicator';
|
||||
|
||||
export const heartbeat: Column<Environment> = {
|
||||
Header: 'Heartbeat',
|
||||
|
@ -16,35 +15,12 @@ export const heartbeat: Column<Environment> = {
|
|||
export function StatusCell({
|
||||
row: { original: environment },
|
||||
}: CellProps<Environment>) {
|
||||
const { disableTrustOnFirstConnect } = useRowContext();
|
||||
|
||||
if (disableTrustOnFirstConnect && !environment.UserTrusted) {
|
||||
return <span className="label label-default">untrusted</span>;
|
||||
}
|
||||
|
||||
if (!environment.LastCheckInDate) {
|
||||
return (
|
||||
<span className="label label-default">
|
||||
<s>associated</s>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<i
|
||||
className={clsx(
|
||||
'fa',
|
||||
'fa-heartbeat',
|
||||
environmentStatusLabel(environment.Status)
|
||||
)}
|
||||
aria-hidden="true"
|
||||
<EdgeIndicator
|
||||
checkInInterval={environment.EdgeCheckinInterval}
|
||||
edgeId={environment.EdgeID}
|
||||
lastCheckInDate={environment.LastCheckInDate}
|
||||
queryDate={environment.QueryDate}
|
||||
/>
|
||||
);
|
||||
|
||||
function environmentStatusLabel(status: EnvironmentStatus) {
|
||||
if (status === EnvironmentStatus.Up) {
|
||||
return 'green-icon';
|
||||
}
|
||||
return 'orange-icon';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
export interface EdgeDeviceTableSettings {
|
||||
hiddenColumns: string[];
|
||||
autoRefreshRate: number;
|
||||
pageSize: number;
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/portainer/components/datatables/types';
|
||||
|
||||
export interface FDOProfilesTableSettings {
|
||||
pageSize: number;
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
export interface EdgeDeviceTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export interface FDOProfilesTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
export enum DeviceAction {
|
||||
PowerOn = 'power on',
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
groups="($ctrl.groups)"
|
||||
is-fdo-enabled="($ctrl.isFDOEnabled)"
|
||||
is-open-amt-enabled="($ctrl.isOpenAMTEnabled)"
|
||||
disable-trust-on-first-connect="($ctrl.disableTrustOnFirstConnect)"
|
||||
show-waiting-room-link="($ctrl.showWaitingRoomLink)"
|
||||
mps-server="($ctrl.mpsServer)"
|
||||
on-refresh="($ctrl.getEnvironments)"
|
||||
set-loading-message="($ctrl.setLoadingMessage)"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { getEndpoints } from 'Portainer/environments/environment.service';
|
||||
import { EnvironmentType } from 'Portainer/environments/types';
|
||||
|
||||
angular.module('portainer.edge').controller('EdgeDevicesViewController', EdgeDevicesViewController);
|
||||
/* @ngInject */
|
||||
|
@ -11,7 +10,7 @@ export function EdgeDevicesViewController($q, $async, EndpointService, GroupServ
|
|||
this.getEnvironments = function () {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const [endpointsResponse, groups] = await Promise.all([getEndpoints(0, 100, { types: [EnvironmentType.EdgeAgentOnDocker] }), GroupService.groups()]);
|
||||
const [endpointsResponse, groups] = await Promise.all([getEndpoints(0, 100, { edgeDeviceFilter: 'trusted' }), GroupService.groups()]);
|
||||
ctrl.groups = groups;
|
||||
ctrl.edgeDevices = endpointsResponse.value;
|
||||
} catch (err) {
|
||||
|
@ -27,7 +26,7 @@ export function EdgeDevicesViewController($q, $async, EndpointService, GroupServ
|
|||
const settings = await SettingsService.settings();
|
||||
|
||||
ctrl.isFDOEnabled = settings && settings.EnableEdgeComputeFeatures && settings.fdoConfiguration && settings.fdoConfiguration.enabled;
|
||||
ctrl.disableTrustOnFirstConnect = settings && settings.EnableEdgeComputeFeatures && settings.DisableTrustOnFirstConnect;
|
||||
ctrl.showWaitingRoomLink = process.env.PORTAINER_EDITION === 'BE' && settings && settings.EnableEdgeComputeFeatures && !settings.TrustOnFirstConnect;
|
||||
ctrl.isOpenAMTEnabled = settings && settings.EnableEdgeComputeFeatures && settings.openAMTConfiguration && settings.openAMTConfiguration.enabled;
|
||||
ctrl.mpsServer = ctrl.isOpenAMTEnabled ? settings.openAMTConfiguration.mpsServer : '';
|
||||
} catch (err) {
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { Row, TableRowProps } from 'react-table';
|
||||
|
||||
interface Props<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||
isLoading?: boolean;
|
||||
rows: Row<T>[];
|
||||
emptyContent?: string;
|
||||
prepareRow(row: Row<T>): void;
|
||||
renderRow(row: Row<T>, rowProps: TableRowProps): React.ReactNode;
|
||||
}
|
||||
|
||||
export function TableContent<
|
||||
T extends Record<string, unknown> = Record<string, unknown>
|
||||
>({
|
||||
isLoading = false,
|
||||
rows,
|
||||
emptyContent = 'No items available',
|
||||
prepareRow,
|
||||
renderRow,
|
||||
}: Props<T>) {
|
||||
if (isLoading) {
|
||||
return <TableContentOneColumn>Loading...</TableContentOneColumn>;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
return <TableContentOneColumn>{emptyContent}</TableContentOneColumn>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{rows.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
return renderRow(row, { key, className, role, style });
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TableContentOneColumn({ children }: PropsWithChildren<unknown>) {
|
||||
// using MAX_SAFE_INTEGER to make sure the single column will be the size of the table
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={Number.MAX_SAFE_INTEGER} className="text-center text-muted">
|
||||
{children}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -1,9 +1,50 @@
|
|||
export { Table } from './Table';
|
||||
export { TableActions } from './TableActions';
|
||||
export { TableTitleActions } from './TableTitleActions';
|
||||
export { TableHeaderCell } from './TableHeaderCell';
|
||||
export { TableSettingsMenu } from './TableSettingsMenu';
|
||||
export { TableTitle } from './TableTitle';
|
||||
export { TableContainer } from './TableContainer';
|
||||
export { TableHeaderRow } from './TableHeaderRow';
|
||||
export { TableRow } from './TableRow';
|
||||
import { Table as MainComponent } from './Table';
|
||||
import { TableActions } from './TableActions';
|
||||
import { TableTitleActions } from './TableTitleActions';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||
import { TableTitle } from './TableTitle';
|
||||
import { TableContainer } from './TableContainer';
|
||||
import { TableHeaderRow } from './TableHeaderRow';
|
||||
import { TableRow } from './TableRow';
|
||||
import { TableContent } from './TableContent';
|
||||
import { TableFooter } from './TableFooter';
|
||||
|
||||
interface SubComponents {
|
||||
Container: typeof TableContainer;
|
||||
Actions: typeof TableActions;
|
||||
TitleActions: typeof TableTitleActions;
|
||||
HeaderCell: typeof TableHeaderCell;
|
||||
SettingsMenu: typeof TableSettingsMenu;
|
||||
Title: typeof TableTitle;
|
||||
Row: typeof TableRow;
|
||||
HeaderRow: typeof TableHeaderRow;
|
||||
Content: typeof TableContent;
|
||||
Footer: typeof TableFooter;
|
||||
}
|
||||
|
||||
const Table: typeof MainComponent & SubComponents =
|
||||
MainComponent as typeof MainComponent & SubComponents;
|
||||
|
||||
Table.Actions = TableActions;
|
||||
Table.TitleActions = TableTitleActions;
|
||||
Table.Container = TableContainer;
|
||||
Table.HeaderCell = TableHeaderCell;
|
||||
Table.SettingsMenu = TableSettingsMenu;
|
||||
Table.Title = TableTitle;
|
||||
Table.Row = TableRow;
|
||||
Table.HeaderRow = TableHeaderRow;
|
||||
Table.Content = TableContent;
|
||||
Table.Footer = TableFooter;
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableActions,
|
||||
TableTitleActions,
|
||||
TableHeaderCell,
|
||||
TableSettingsMenu,
|
||||
TableTitle,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
export interface PaginationTableSettings {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface SortableTableSettings {
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
|
||||
export interface SettableColumnsTableSettings {
|
||||
hiddenColumns: string[];
|
||||
}
|
||||
|
||||
export interface SettableQuickActionsTableSettings<TAction> {
|
||||
hiddenQuickActions: TAction[];
|
||||
}
|
||||
|
||||
export interface RefreshableTableSettings {
|
||||
autoRefreshRate: number;
|
||||
}
|
|
@ -13,20 +13,20 @@ import type {
|
|||
|
||||
import { arrayToJson, buildUrl } from './utils';
|
||||
|
||||
interface EndpointsQuery {
|
||||
export interface EnvironmentsQueryParams {
|
||||
search?: string;
|
||||
types?: EnvironmentType[];
|
||||
tagIds?: TagId[];
|
||||
endpointIds?: EnvironmentId[];
|
||||
tagsPartialMatch?: boolean;
|
||||
groupId?: EnvironmentGroupId;
|
||||
edgeDeviceFilter?: boolean;
|
||||
edgeDeviceFilter?: 'all' | 'trusted' | 'untrusted';
|
||||
}
|
||||
|
||||
export async function getEndpoints(
|
||||
start: number,
|
||||
limit: number,
|
||||
{ types, tagIds, endpointIds, ...query }: EndpointsQuery = {}
|
||||
{ types, tagIds, endpointIds, ...query }: EnvironmentsQueryParams = {}
|
||||
) {
|
||||
if (tagIds && tagIds.length === 0) {
|
||||
return { totalCount: 0, value: <Environment[]>[] };
|
||||
|
|
|
@ -1,22 +1,27 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { getEndpoints } from '@/portainer/environments/environment.service';
|
||||
import {
|
||||
EnvironmentsQueryParams,
|
||||
getEndpoints,
|
||||
} from '@/portainer/environments/environment.service';
|
||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
||||
import { error as notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
const ENVIRONMENTS_POLLING_INTERVAL = 30000; // in ms
|
||||
|
||||
export function useEnvironmentList(
|
||||
page: number,
|
||||
pageLimit: number,
|
||||
textFilter: string,
|
||||
refetchOffline = false
|
||||
) {
|
||||
interface Query extends EnvironmentsQueryParams {
|
||||
page?: number;
|
||||
pageLimit?: number;
|
||||
}
|
||||
|
||||
export function useEnvironmentList(query: Query = {}, refetchOffline = false) {
|
||||
const { page = 1, pageLimit = 100 } = query;
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
['environments', page, pageLimit, textFilter],
|
||||
['environments', { page, pageLimit, ...query }],
|
||||
async () => {
|
||||
const start = (page - 1) * pageLimit + 1;
|
||||
return getEndpoints(start, pageLimit, { search: textFilter });
|
||||
return getEndpoints(start, pageLimit, query);
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { promiseSequence } from './promise-utils';
|
||||
|
||||
describe('promiseSequence', () => {
|
||||
it('should run successfully for an empty list', async () => {
|
||||
await expect(promiseSequence([])).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('provided two promise functions, the second should run after the first', async () => {
|
||||
const callback = jest.fn();
|
||||
|
||||
function first() {
|
||||
return Promise.resolve(callback(1));
|
||||
}
|
||||
|
||||
function second() {
|
||||
return Promise.resolve(callback(2));
|
||||
}
|
||||
|
||||
await promiseSequence([first, second]);
|
||||
expect(callback).toHaveBeenCalledTimes(2);
|
||||
expect(callback).toHaveBeenNthCalledWith(1, 1);
|
||||
expect(callback).toHaveBeenNthCalledWith(2, 2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* runs the provided promises in a sequence, and returns a promise that resolves when all promises have resolved
|
||||
*
|
||||
* @param promises a list of functions that return promises
|
||||
*/
|
||||
export function promiseSequence<T>(promises: (() => Promise<T>)[]) {
|
||||
return promises.reduce(
|
||||
(promise, nextPromise) => promise.then(() => nextPromise()),
|
||||
Promise.resolve<T>(undefined as unknown as T)
|
||||
);
|
||||
}
|
|
@ -47,7 +47,7 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
const groupsQuery = useGroups();
|
||||
|
||||
const { isLoading, environments, totalCount, totalAvailable } =
|
||||
useEnvironmentList(page, pageLimit, debouncedTextFilter, true);
|
||||
useEnvironmentList({ page, pageLimit, search: debouncedTextFilter }, true);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -15,7 +15,7 @@ export function SettingsViewModel(data) {
|
|||
this.EnableTelemetry = data.EnableTelemetry;
|
||||
this.KubeconfigExpiry = data.KubeconfigExpiry;
|
||||
this.HelmRepositoryURL = data.HelmRepositoryURL;
|
||||
this.DisableTrustOnFirstConnect = data.DisableTrustOnFirstConnect;
|
||||
this.TrustOnFirstConnect = data.TrustOnFirstConnect;
|
||||
this.EnforceEdgeID = data.EnforceEdgeID;
|
||||
this.AgentSecret = data.AgentSecret;
|
||||
this.EdgePortainerUrl = data.EdgePortainerUrl;
|
||||
|
@ -24,7 +24,6 @@ export function SettingsViewModel(data) {
|
|||
export function PublicSettingsViewModel(settings) {
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
this.DisableTrustOnFirstConnect = settings.DisableTrustOnFirstConnect;
|
||||
this.EnforceEdgeID = settings.EnforceEdgeID;
|
||||
this.FeatureFlagSettings = settings.FeatureFlagSettings;
|
||||
this.LogoURL = settings.LogoURL;
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
import { Field, Form, Formik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { useUpdateSettingsMutation } from '@/portainer/settings/settings.service';
|
||||
|
||||
import { Settings } from '../types';
|
||||
|
||||
import { EnabledWaitingRoomSwitch } from './EnableWaitingRoomSwitch';
|
||||
|
||||
interface FormValues {
|
||||
EdgePortainerUrl: string;
|
||||
TrustOnFirstConnect: boolean;
|
||||
}
|
||||
const validation = yup.object({
|
||||
TrustOnFirstConnect: yup.boolean().required('This field is required.'),
|
||||
EdgePortainerUrl: yup
|
||||
.string()
|
||||
.test(
|
||||
'not-local',
|
||||
'Cannot use localhost as environment URL',
|
||||
(value) => !value?.includes('localhost')
|
||||
)
|
||||
.url('URL should be a valid URI')
|
||||
.required('URL is required'),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
const defaultUrl = buildDefaultUrl();
|
||||
|
||||
export function AutoEnvCreationSettingsForm({ settings }: Props) {
|
||||
const url = settings.EdgePortainerUrl;
|
||||
|
||||
const initialValues = {
|
||||
EdgePortainerUrl: url || defaultUrl,
|
||||
TrustOnFirstConnect: settings.TrustOnFirstConnect,
|
||||
};
|
||||
|
||||
const mutation = useUpdateSettingsMutation();
|
||||
|
||||
const { mutate: updateSettings } = mutation;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(variables: Partial<FormValues>) => {
|
||||
updateSettings(variables, {
|
||||
onSuccess() {
|
||||
notifySuccess(
|
||||
'Successfully updated Automatic Environment Creation settings'
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url && validation.isValidSync({ url: defaultUrl })) {
|
||||
handleSubmit({ EdgePortainerUrl: defaultUrl });
|
||||
}
|
||||
}, [handleSubmit, url]);
|
||||
|
||||
return (
|
||||
<Formik<FormValues>
|
||||
initialValues={initialValues}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validation}
|
||||
validateOnMount
|
||||
enableReinitialize
|
||||
>
|
||||
{({ errors, isValid, dirty }) => (
|
||||
<Form className="form-horizontal">
|
||||
<FormSectionTitle>Configuration</FormSectionTitle>
|
||||
|
||||
<FormControl
|
||||
label="Portainer URL"
|
||||
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
|
||||
inputId="url-input"
|
||||
errors={errors.EdgePortainerUrl}
|
||||
>
|
||||
<Field as={Input} id="url-input" name="EdgePortainerUrl" />
|
||||
</FormControl>
|
||||
|
||||
<EnabledWaitingRoomSwitch />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
loadingText="generating..."
|
||||
isLoading={mutation.isLoading}
|
||||
disabled={!isValid || !dirty}
|
||||
>
|
||||
Save Settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
function buildDefaultUrl() {
|
||||
const baseHREF = baseHref();
|
||||
return window.location.origin + (baseHREF !== '/' ? baseHREF : '');
|
||||
}
|
|
@ -4,15 +4,27 @@ import { useEffect } from 'react';
|
|||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { EdgeScriptForm } from '@/edge/components/EdgeScriptForm';
|
||||
import { generateKey } from '@/portainer/environments/environment.service/edge';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
|
||||
import { useSettings } from '../../settings.service';
|
||||
|
||||
import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm';
|
||||
|
||||
export function AutomaticEdgeEnvCreation() {
|
||||
const edgeKeyMutation = useGenerateKeyMutation();
|
||||
const { mutate } = edgeKeyMutation;
|
||||
const { mutate: generateKey } = edgeKeyMutation;
|
||||
const settingsQuery = useSettings();
|
||||
|
||||
const url = settingsQuery.data?.EdgePortainerUrl;
|
||||
|
||||
useEffect(() => {
|
||||
mutate();
|
||||
}, [mutate]);
|
||||
if (url) {
|
||||
generateKey();
|
||||
}
|
||||
}, [generateKey, url]);
|
||||
|
||||
if (!settingsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const edgeKey = edgeKeyMutation.data;
|
||||
|
||||
|
@ -23,17 +35,19 @@ export function AutomaticEdgeEnvCreation() {
|
|||
title="Automatic Edge Environment Creation"
|
||||
/>
|
||||
<WidgetBody>
|
||||
{edgeKey ? (
|
||||
<EdgeScriptForm edgeKey={edgeKey} />
|
||||
<AutoEnvCreationSettingsForm settings={settingsQuery.data} />
|
||||
|
||||
{edgeKeyMutation.isLoading ? (
|
||||
<div>Generating key for {url} ... </div>
|
||||
) : (
|
||||
<TextTip>Please choose a valid edge portainer URL</TextTip>
|
||||
edgeKey && <EdgeScriptForm edgeKey={edgeKey} />
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
// using mutation because this action generates an object (although it's not saved in db)
|
||||
// using mutation because we want this action to run only when required
|
||||
function useGenerateKeyMutation() {
|
||||
return useMutation(generateKey);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { useField } from 'formik';
|
||||
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||
import { confirmAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
export function EnabledWaitingRoomSwitch() {
|
||||
const [inputProps, meta, helpers] = useField<boolean>('TrustOnFirstConnect');
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
inputId="edge_waiting_room"
|
||||
label="Disable Edge Environment Waiting Room"
|
||||
errors={meta.error}
|
||||
>
|
||||
<Switch
|
||||
id="edge_waiting_room"
|
||||
name="TrustOnFirstConnect"
|
||||
className="space-right"
|
||||
checked={inputProps.value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
async function handleChange(trust: boolean) {
|
||||
if (!trust) {
|
||||
helpers.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await confirmAsync({
|
||||
title: 'Disable Edge Environment Waiting Room',
|
||||
message:
|
||||
'By disabling the waiting room feature, all devices requesting association will be automatically associated and could pose a security risk. Are you sure?',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
},
|
||||
confirm: {
|
||||
label: 'Confirm',
|
||||
className: 'btn-danger',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
helpers.setValue(!!confirmed);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Formik, Form, Field } from 'formik';
|
||||
import { Formik, Form } from 'formik';
|
||||
|
||||
import { Switch } from '@/portainer/components/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||
|
@ -6,8 +6,6 @@ import { Select } from '@/portainer/components/form-components/Input/Select';
|
|||
import { Widget, WidgetBody, WidgetTitle } from '@/portainer/components/widget';
|
||||
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||
import { Input } from '@/portainer/components/form-components/Input';
|
||||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
import { Settings } from '../types';
|
||||
|
||||
|
@ -17,9 +15,7 @@ import { validationSchema } from './EdgeComputeSettings.validation';
|
|||
export interface FormValues {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
DisableTrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -50,9 +46,7 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
|||
const initialValues: FormValues = {
|
||||
EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
DisableTrustOnFirstConnect: settings.DisableTrustOnFirstConnect,
|
||||
EnforceEdgeID: settings.EnforceEdgeID,
|
||||
EdgePortainerUrl: settings.EdgePortainerUrl || buildDefaultUrl(),
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -124,8 +118,9 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
|||
|
||||
<FormControl
|
||||
inputId="edge_enforce_id"
|
||||
label="Enforce use of Portainer generated Edge ID’s"
|
||||
label="Enforce use of Portainer generated Edge ID"
|
||||
size="medium"
|
||||
tooltip="This setting only applies to manually created environments."
|
||||
errors={errors.EnforceEdgeID}
|
||||
>
|
||||
<Switch
|
||||
|
@ -139,16 +134,6 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
|||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Portainer URL"
|
||||
tooltip="URL of the Portainer instance that the agent will use to initiate the communications."
|
||||
inputId="url-input"
|
||||
errors={errors.EdgePortainerUrl}
|
||||
size="medium"
|
||||
>
|
||||
<Field as={Input} id="url-input" name="EdgePortainerUrl" />
|
||||
</FormControl>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
|
@ -170,8 +155,3 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildDefaultUrl() {
|
||||
const base = baseHref();
|
||||
return window.location.origin + (base !== '/' ? base : '');
|
||||
}
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
import { boolean, number, object, string } from 'yup';
|
||||
import { boolean, number, object } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
EdgeAgentCheckinInterval: number().required('This field is required.'),
|
||||
EnableEdgeComputeFeatures: boolean().required('This field is required.'),
|
||||
DisableTrustOnFirstConnect: boolean().required('This field is required.'),
|
||||
EnforceEdgeID: boolean().required('This field is required.'),
|
||||
EdgePortainerUrl: string()
|
||||
.test(
|
||||
'notlocal',
|
||||
'Cannot use localhost as environment URL',
|
||||
(value) => !value?.includes('localhost')
|
||||
)
|
||||
.url('URL should be a valid URI')
|
||||
.required('URL is required'),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export interface Settings {
|
||||
EdgeAgentCheckinInterval: number;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
DisableTrustOnFirstConnect: boolean;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useQuery } from 'react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
|
||||
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||
|
||||
|
@ -25,7 +25,7 @@ enum AuthenticationMethod {
|
|||
AuthenticationOAuth,
|
||||
}
|
||||
|
||||
interface SettingsResponse {
|
||||
export interface Settings {
|
||||
LogoURL: string;
|
||||
BlackListedLabels: { name: string; value: string }[];
|
||||
AuthenticationMethod: AuthenticationMethod;
|
||||
|
@ -38,14 +38,15 @@ interface SettingsResponse {
|
|||
EnableTelemetry: boolean;
|
||||
HelmRepositoryURL: string;
|
||||
KubectlShellImage: string;
|
||||
DisableTrustOnFirstConnect: boolean;
|
||||
TrustOnFirstConnect: boolean;
|
||||
EnforceEdgeID: boolean;
|
||||
AgentSecret: string;
|
||||
EdgePortainerUrl: string;
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
try {
|
||||
const { data } = await axios.get<SettingsResponse>(buildUrl());
|
||||
const { data } = await axios.get<Settings>(buildUrl());
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(
|
||||
|
@ -55,9 +56,31 @@ export async function getSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
export function useSettings<T = SettingsResponse>(
|
||||
select?: (settings: SettingsResponse) => T
|
||||
) {
|
||||
async function updateSettings(settings: Partial<Settings>) {
|
||||
try {
|
||||
await axios.put(buildUrl(), settings);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update application settings');
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateSettingsMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(updateSettings, {
|
||||
onSuccess() {
|
||||
return queryClient.invalidateQueries(['settings']);
|
||||
},
|
||||
meta: {
|
||||
error: {
|
||||
title: 'Failure',
|
||||
message: 'Unable to update settings',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSettings<T = Settings>(select?: (settings: Settings) => T) {
|
||||
return useQuery(['settings'], getSettings, { select });
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue