mirror of https://github.com/portainer/portainer
refactor(containers): migrate view to react [EE-2212] (#6577)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>pull/7382/head
parent
5ee570e075
commit
bed4257194
|
@ -0,0 +1,86 @@
|
||||||
|
package containers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
containertypes "github.com/docker/docker/api/types/container"
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
portaineree "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type containerGpusResponse struct {
|
||||||
|
Gpus string `json:"gpus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @id dockerContainerGpusInspect
|
||||||
|
// @summary Fetch container gpus data
|
||||||
|
// @description
|
||||||
|
// @description **Access policy**:
|
||||||
|
// @tags docker
|
||||||
|
// @security jwt
|
||||||
|
// @accept json
|
||||||
|
// @produce json
|
||||||
|
// @param environmentId path int true "Environment identifier"
|
||||||
|
// @param containerId path int true "Container identifier"
|
||||||
|
// @success 200 {object} containerGpusResponse "Success"
|
||||||
|
// @failure 404 "Environment or container not found"
|
||||||
|
// @failure 400 "Bad request"
|
||||||
|
// @failure 500 "Internal server error"
|
||||||
|
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
|
||||||
|
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
|
||||||
|
if err != nil {
|
||||||
|
return httperror.BadRequest("Invalid container identifier route variable", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := middlewares.FetchEndpoint(r)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.NotFound("Unable to find an environment on request context", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
|
||||||
|
|
||||||
|
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := cli.ContainerInspect(r.Context(), containerId)
|
||||||
|
if err != nil {
|
||||||
|
return httperror.NotFound("Unable to find the container", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if container.HostConfig == nil {
|
||||||
|
return httperror.NotFound("Unable to find the container host config", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
|
||||||
|
if opt.Driver == "nvidia" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.Capabilities[0][0] == "gpu"
|
||||||
|
})
|
||||||
|
|
||||||
|
if gpuOptionsIndex == -1 {
|
||||||
|
return response.JSON(w, containerGpusResponse{Gpus: "none"})
|
||||||
|
}
|
||||||
|
|
||||||
|
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
|
||||||
|
|
||||||
|
gpu := "all"
|
||||||
|
if gpuOptions.Count != -1 {
|
||||||
|
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, containerGpusResponse{Gpus: gpu})
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package containers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
*mux.Router
|
||||||
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||||
|
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||||
|
h := &Handler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
|
||||||
|
dockerClientFactory: dockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
router := h.PathPrefix(routePrefix).Subrouter()
|
||||||
|
router.Use(bouncer.AuthenticatedAccess)
|
||||||
|
|
||||||
|
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/http/handler/docker/containers"
|
||||||
|
"github.com/portainer/portainer/api/http/middlewares"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
|
||||||
|
type Handler struct {
|
||||||
|
*mux.Router
|
||||||
|
requestBouncer *security.RequestBouncer
|
||||||
|
dataStore dataservices.DataStore
|
||||||
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
authorizationService *authorization.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
|
||||||
|
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
|
||||||
|
h := &Handler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
requestBouncer: bouncer,
|
||||||
|
authorizationService: authorizationService,
|
||||||
|
dataStore: dataStore,
|
||||||
|
dockerClientFactory: dockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
// endpoints
|
||||||
|
endpointRouter := h.PathPrefix("/{id}").Subrouter()
|
||||||
|
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
|
||||||
|
endpointRouter.Use(dockerOnlyMiddleware)
|
||||||
|
|
||||||
|
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
|
||||||
|
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func dockerOnlyMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
|
||||||
|
endpoint, err := middlewares.FetchEndpoint(request)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !endpointutils.IsDockerEndpoint(endpoint) {
|
||||||
|
errMessage := "environment is not a docker environment"
|
||||||
|
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(rw, request)
|
||||||
|
})
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/handler/auth"
|
"github.com/portainer/portainer/api/http/handler/auth"
|
||||||
"github.com/portainer/portainer/api/http/handler/backup"
|
"github.com/portainer/portainer/api/http/handler/backup"
|
||||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||||
|
"github.com/portainer/portainer/api/http/handler/docker"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||||
|
@ -45,6 +46,7 @@ type Handler struct {
|
||||||
AuthHandler *auth.Handler
|
AuthHandler *auth.Handler
|
||||||
BackupHandler *backup.Handler
|
BackupHandler *backup.Handler
|
||||||
CustomTemplatesHandler *customtemplates.Handler
|
CustomTemplatesHandler *customtemplates.Handler
|
||||||
|
DockerHandler *docker.Handler
|
||||||
EdgeGroupsHandler *edgegroups.Handler
|
EdgeGroupsHandler *edgegroups.Handler
|
||||||
EdgeJobsHandler *edgejobs.Handler
|
EdgeJobsHandler *edgejobs.Handler
|
||||||
EdgeStacksHandler *edgestacks.Handler
|
EdgeStacksHandler *edgestacks.Handler
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/handler/auth"
|
"github.com/portainer/portainer/api/http/handler/auth"
|
||||||
"github.com/portainer/portainer/api/http/handler/backup"
|
"github.com/portainer/portainer/api/http/handler/backup"
|
||||||
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
"github.com/portainer/portainer/api/http/handler/customtemplates"
|
||||||
|
dockerhandler "github.com/portainer/portainer/api/http/handler/docker"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
"github.com/portainer/portainer/api/http/handler/edgejobs"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||||
|
@ -184,6 +185,8 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
|
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
|
||||||
|
|
||||||
|
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
|
||||||
|
|
||||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
|
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
|
||||||
|
|
||||||
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
|
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
|
||||||
|
@ -275,6 +278,7 @@ func (server *Server) Start() error {
|
||||||
AuthHandler: authHandler,
|
AuthHandler: authHandler,
|
||||||
BackupHandler: backupHandler,
|
BackupHandler: backupHandler,
|
||||||
CustomTemplatesHandler: customTemplatesHandler,
|
CustomTemplatesHandler: customTemplatesHandler,
|
||||||
|
DockerHandler: dockerHandler,
|
||||||
EdgeGroupsHandler: edgeGroupsHandler,
|
EdgeGroupsHandler: edgeGroupsHandler,
|
||||||
EdgeJobsHandler: edgeJobsHandler,
|
EdgeJobsHandler: edgeJobsHandler,
|
||||||
EdgeStacksHandler: edgeStacksHandler,
|
EdgeStacksHandler: edgeStacksHandler,
|
||||||
|
|
|
@ -96,94 +96,6 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var containers = {
|
|
||||||
name: 'docker.containers',
|
|
||||||
url: '/containers',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/containers.html',
|
|
||||||
controller: 'ContainersController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var container = {
|
|
||||||
name: 'docker.containers.container',
|
|
||||||
url: '/:id?nodeName',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/edit/container.html',
|
|
||||||
controller: 'ContainerController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerAttachConsole = {
|
|
||||||
name: 'docker.containers.container.attach',
|
|
||||||
url: '/attach',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/console/attach.html',
|
|
||||||
controller: 'ContainerConsoleController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerExecConsole = {
|
|
||||||
name: 'docker.containers.container.exec',
|
|
||||||
url: '/exec',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/console/exec.html',
|
|
||||||
controller: 'ContainerConsoleController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerCreation = {
|
|
||||||
name: 'docker.containers.new',
|
|
||||||
url: '/new?nodeName&from',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/create/createcontainer.html',
|
|
||||||
controller: 'CreateContainerController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerInspect = {
|
|
||||||
name: 'docker.containers.container.inspect',
|
|
||||||
url: '/inspect',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/inspect/containerinspect.html',
|
|
||||||
controller: 'ContainerInspectController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerLogs = {
|
|
||||||
name: 'docker.containers.container.logs',
|
|
||||||
url: '/logs',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/logs/containerlogs.html',
|
|
||||||
controller: 'ContainerLogsController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
var containerStats = {
|
|
||||||
name: 'docker.containers.container.stats',
|
|
||||||
url: '/stats',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: './views/containers/stats/containerstats.html',
|
|
||||||
controller: 'ContainerStatsController',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const customTemplates = {
|
const customTemplates = {
|
||||||
name: 'docker.templates.custom',
|
name: 'docker.templates.custom',
|
||||||
url: '/custom',
|
url: '/custom',
|
||||||
|
@ -613,14 +525,7 @@ angular.module('portainer.docker', ['portainer.app', reactModule]).config([
|
||||||
$stateRegistryProvider.register(configs);
|
$stateRegistryProvider.register(configs);
|
||||||
$stateRegistryProvider.register(config);
|
$stateRegistryProvider.register(config);
|
||||||
$stateRegistryProvider.register(configCreation);
|
$stateRegistryProvider.register(configCreation);
|
||||||
$stateRegistryProvider.register(containers);
|
|
||||||
$stateRegistryProvider.register(container);
|
|
||||||
$stateRegistryProvider.register(containerExecConsole);
|
|
||||||
$stateRegistryProvider.register(containerAttachConsole);
|
|
||||||
$stateRegistryProvider.register(containerCreation);
|
|
||||||
$stateRegistryProvider.register(containerInspect);
|
|
||||||
$stateRegistryProvider.register(containerLogs);
|
|
||||||
$stateRegistryProvider.register(containerStats);
|
|
||||||
$stateRegistryProvider.register(customTemplates);
|
$stateRegistryProvider.register(customTemplates);
|
||||||
$stateRegistryProvider.register(customTemplatesNew);
|
$stateRegistryProvider.register(customTemplatesNew);
|
||||||
$stateRegistryProvider.register(customTemplatesEdit);
|
$stateRegistryProvider.register(customTemplatesEdit);
|
||||||
|
|
|
@ -1,24 +1,13 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
import { ContainersDatatableContainer } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableContainer';
|
import { StackContainersDatatable } from '@/react/docker/stacks/ItemView/StackContainersDatatable';
|
||||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
import { TemplateListDropdownAngular } from '@/react/docker/app-templates/TemplateListDropdown';
|
||||||
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
|
import { TemplateListSortAngular } from '@/react/docker/app-templates/TemplateListSort';
|
||||||
|
|
||||||
export const componentsModule = angular
|
export const componentsModule = angular
|
||||||
.module('portainer.docker.react.components', [])
|
.module('portainer.docker.react.components', [])
|
||||||
.component(
|
|
||||||
'containersDatatable',
|
|
||||||
r2a(ContainersDatatableContainer, [
|
|
||||||
'endpoint',
|
|
||||||
'isAddActionVisible',
|
|
||||||
'dataset',
|
|
||||||
'onRefresh',
|
|
||||||
'isHostColumnVisible',
|
|
||||||
'tableKey',
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.component(
|
.component(
|
||||||
'containerQuickActions',
|
'containerQuickActions',
|
||||||
r2a(ContainerQuickActions, [
|
r2a(ContainerQuickActions, [
|
||||||
|
@ -30,4 +19,8 @@ export const componentsModule = angular
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component('templateListDropdown', TemplateListDropdownAngular)
|
.component('templateListDropdown', TemplateListDropdownAngular)
|
||||||
.component('templateListSort', TemplateListSortAngular).name;
|
.component('templateListSort', TemplateListSortAngular)
|
||||||
|
.component(
|
||||||
|
'stackContainersDatatable',
|
||||||
|
r2a(StackContainersDatatable, ['environment', 'stackName'])
|
||||||
|
).name;
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { StateRegistry } from '@uirouter/angularjs';
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { ListView } from '@/react/docker/containers/ListView';
|
||||||
|
|
||||||
|
export const containersModule = angular
|
||||||
|
.module('portainer.docker.containers', [])
|
||||||
|
.component('containersView', r2a(ListView, ['endpoint']))
|
||||||
|
|
||||||
|
.config(config).name;
|
||||||
|
|
||||||
|
/* @ngInject */
|
||||||
|
function config($stateRegistryProvider: StateRegistry) {
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers',
|
||||||
|
url: '/containers',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
component: 'containersView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container',
|
||||||
|
url: '/:id?nodeName',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/edit/container.html',
|
||||||
|
controller: 'ContainerController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container.attach',
|
||||||
|
url: '/attach',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/console/attach.html',
|
||||||
|
controller: 'ContainerConsoleController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container.exec',
|
||||||
|
url: '/exec',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/console/exec.html',
|
||||||
|
controller: 'ContainerConsoleController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.new',
|
||||||
|
url: '/new?nodeName&from',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/create/createcontainer.html',
|
||||||
|
controller: 'CreateContainerController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container.inspect',
|
||||||
|
url: '/inspect',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/inspect/containerinspect.html',
|
||||||
|
controller: 'ContainerInspectController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container.logs',
|
||||||
|
url: '/logs',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/logs/containerlogs.html',
|
||||||
|
controller: 'ContainerLogsController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$stateRegistryProvider.register({
|
||||||
|
name: 'docker.containers.container.stats',
|
||||||
|
url: '/stats',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: '~@/docker/views/containers/stats/containerstats.html',
|
||||||
|
controller: 'ContainerStatsController',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { Gpu } from 'Docker/react/views/gpu';
|
import { Gpu } from 'Docker/react/views/gpu';
|
||||||
|
|
||||||
import { ItemView } from '@/react/docker/networks/ItemView';
|
import { ItemView as NetworksItemView } from '@/react/docker/networks/ItemView';
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
|
||||||
|
import { containersModule } from './containers';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.docker.react.views', [])
|
.module('portainer.docker.react.views', [containersModule])
|
||||||
.component(
|
.component(
|
||||||
'gpu',
|
'gpu',
|
||||||
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
r2a(Gpu, ['values', 'onChange', 'gpus', 'usedGpus', 'usedAllGpus'])
|
||||||
)
|
)
|
||||||
.component('networkDetailsView', r2a(ItemView, [])).name;
|
.component('networkDetailsView', r2a(NetworksItemView, [])).name;
|
||||||
|
|
|
@ -36,20 +36,6 @@ export async function getInfo(environmentId: EnvironmentId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUrl(
|
|
||||||
environmentId: EnvironmentId,
|
|
||||||
action: string,
|
|
||||||
subAction = ''
|
|
||||||
) {
|
|
||||||
let url = `/endpoints/${environmentId}/docker/${action}`;
|
|
||||||
|
|
||||||
if (subAction) {
|
|
||||||
url += `/${subAction}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useInfo<TSelect = InfoResponse>(
|
export function useInfo<TSelect = InfoResponse>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
select?: (info: InfoResponse) => TSelect
|
select?: (info: InfoResponse) => TSelect
|
||||||
|
@ -74,3 +60,17 @@ export function useVersion<TSelect = VersionResponse>(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildUrl(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
action: string,
|
||||||
|
subAction = ''
|
||||||
|
) {
|
||||||
|
let url = `/endpoints/${environmentId}/docker/${action}`;
|
||||||
|
|
||||||
|
if (subAction) {
|
||||||
|
url += `/${subAction}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<page-header title="'Container list'" breadcrumbs="['Containers']" reload="true"> </page-header>
|
|
||||||
|
|
||||||
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12" ng-if="containers">
|
|
||||||
<containers-datatable
|
|
||||||
endpoint="endpoint"
|
|
||||||
dataset="containers"
|
|
||||||
is-host-column-visible="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
|
||||||
is-add-action-visible="true"
|
|
||||||
on-refresh="(getContainers)"
|
|
||||||
></containers-datatable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,60 +0,0 @@
|
||||||
angular.module('portainer.docker').controller('ContainersController', ContainersController);
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
/* @ngInject */
|
|
||||||
function ContainersController($scope, ContainerService, Notifications, endpoint) {
|
|
||||||
$scope.offlineMode = endpoint.Status !== 1;
|
|
||||||
$scope.endpoint = endpoint;
|
|
||||||
|
|
||||||
$scope.getContainers = getContainers;
|
|
||||||
|
|
||||||
function getContainers() {
|
|
||||||
$scope.containers = null;
|
|
||||||
$scope.containers_t = null;
|
|
||||||
ContainerService.containers(1)
|
|
||||||
.then(function success(data) {
|
|
||||||
$scope.containers_t = data;
|
|
||||||
if ($scope.containers_t.length === 0) {
|
|
||||||
$scope.containers = $scope.containers_t;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let item of $scope.containers_t) {
|
|
||||||
ContainerService.container(item.Id).then(function success(data) {
|
|
||||||
var Id = data.Id;
|
|
||||||
for (var i = 0; i < $scope.containers_t.length; i++) {
|
|
||||||
if (Id == $scope.containers_t[i].Id) {
|
|
||||||
const gpuOptions = _.find(data.HostConfig.DeviceRequests, function (o) {
|
|
||||||
return o.Driver === 'nvidia' || o.Capabilities[0][0] === 'gpu';
|
|
||||||
});
|
|
||||||
if (!gpuOptions) {
|
|
||||||
$scope.containers_t[i]['Gpus'] = 'none';
|
|
||||||
} else {
|
|
||||||
let gpuStr = 'all';
|
|
||||||
if (gpuOptions.Count !== -1) {
|
|
||||||
gpuStr = `id:${_.join(gpuOptions.DeviceIDs, ',')}`;
|
|
||||||
}
|
|
||||||
$scope.containers_t[i]['Gpus'] = `${gpuStr}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let item of $scope.containers_t) {
|
|
||||||
if (!Object.keys(item).includes('Gpus')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$scope.containers = $scope.containers_t;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
|
||||||
$scope.containers = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initView() {
|
|
||||||
getContainers();
|
|
||||||
}
|
|
||||||
|
|
||||||
initView();
|
|
||||||
}
|
|
|
@ -1,11 +1,8 @@
|
||||||
import {
|
import { PropsWithChildren, useMemo, useReducer } from 'react';
|
||||||
createContext,
|
|
||||||
useContext,
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
useMemo,
|
|
||||||
useReducer,
|
import { createRowContext } from '@@/datatables/RowContext';
|
||||||
PropsWithChildren,
|
|
||||||
} from 'react';
|
|
||||||
import { EnvironmentId } from 'Portainer/environments/types';
|
|
||||||
|
|
||||||
interface RowContextState {
|
interface RowContextState {
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
|
@ -13,31 +10,29 @@ interface RowContextState {
|
||||||
toggleIsLoading(): void;
|
toggleIsLoading(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RowContext = createContext<RowContextState | null>(null);
|
const { RowProvider: InternalProvider, useRowContext } =
|
||||||
|
createRowContext<RowContextState>();
|
||||||
|
|
||||||
export interface RowProviderProps {
|
export { useRowContext };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
environmentId: EnvironmentId;
|
environmentId: EnvironmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RowProvider({
|
export function RowProvider({
|
||||||
environmentId,
|
environmentId,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<RowProviderProps>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const [isLoading, toggleIsLoading] = useReducer((state) => !state, false);
|
const [isLoading, toggleIsLoading] = useReducer((state) => !state, false);
|
||||||
|
|
||||||
const state = useMemo(
|
const context = useMemo(
|
||||||
() => ({ isLoading, toggleIsLoading, environmentId }),
|
() => ({
|
||||||
[isLoading, toggleIsLoading, environmentId]
|
isLoading,
|
||||||
|
toggleIsLoading,
|
||||||
|
environmentId,
|
||||||
|
}),
|
||||||
|
[environmentId, isLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <RowContext.Provider value={state}>{children}</RowContext.Provider>;
|
return <InternalProvider context={context}>{children}</InternalProvider>;
|
||||||
}
|
|
||||||
|
|
||||||
export function useRowContext() {
|
|
||||||
const context = useContext(RowContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('should be nested under RowProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,8 +197,7 @@ export function EdgeDevicesDatatable({
|
||||||
return (
|
return (
|
||||||
<RowProvider
|
<RowProvider
|
||||||
key={key}
|
key={key}
|
||||||
isOpenAmtEnabled={isOpenAmtEnabled}
|
context={{ isOpenAmtEnabled, groupName: group[0]?.Name }}
|
||||||
groupName={group[0]?.Name}
|
|
||||||
>
|
>
|
||||||
<TableRow<Environment>
|
<TableRow<Environment>
|
||||||
cells={row.cells}
|
cells={row.cells}
|
||||||
|
|
|
@ -1,35 +1,10 @@
|
||||||
import { createContext, useContext, useMemo, PropsWithChildren } from 'react';
|
import { createRowContext } from '@@/datatables/RowContext';
|
||||||
|
|
||||||
interface RowContextState {
|
interface RowContextState {
|
||||||
isOpenAmtEnabled: boolean;
|
isOpenAmtEnabled: boolean;
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RowContext = createContext<RowContextState | null>(null);
|
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||||
|
|
||||||
export interface RowProviderProps {
|
export { RowProvider, useRowContext };
|
||||||
groupName?: string;
|
|
||||||
isOpenAmtEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RowProvider({
|
|
||||||
groupName,
|
|
||||||
isOpenAmtEnabled,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<RowProviderProps>) {
|
|
||||||
const state = useMemo(
|
|
||||||
() => ({ groupName, isOpenAmtEnabled }),
|
|
||||||
[groupName, isOpenAmtEnabled]
|
|
||||||
);
|
|
||||||
|
|
||||||
return <RowContext.Provider value={state}>{children}</RowContext.Provider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRowContext() {
|
|
||||||
const context = useContext(RowContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('should be nested under RowProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
RefreshableTableSettings,
|
RefreshableTableSettings,
|
||||||
SettableColumnsTableSettings,
|
SettableColumnsTableSettings,
|
||||||
SortableTableSettings,
|
SortableTableSettings,
|
||||||
} from '@@/datatables/types';
|
} from '@@/datatables/types-old';
|
||||||
|
|
||||||
export interface Pagination {
|
export interface Pagination {
|
||||||
pageLimit: number;
|
pageLimit: number;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
PaginationTableSettings,
|
PaginationTableSettings,
|
||||||
SortableTableSettings,
|
SortableTableSettings,
|
||||||
} from '@@/datatables/types';
|
} from '@@/datatables/types-old';
|
||||||
|
|
||||||
export interface TableSettings
|
export interface TableSettings
|
||||||
extends SortableTableSettings,
|
extends SortableTableSettings,
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { createContext, ReactNode, useContext } from 'react';
|
|
||||||
|
|
||||||
import type { Environment } from './types';
|
|
||||||
|
|
||||||
const EnvironmentContext = createContext<Environment | null>(null);
|
|
||||||
|
|
||||||
export function useEnvironment() {
|
|
||||||
const context = useContext(EnvironmentContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error('must be nested under EnvironmentProvider');
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: ReactNode;
|
|
||||||
environment: Environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnvironmentProvider({ children, environment }: Props) {
|
|
||||||
return (
|
|
||||||
<EnvironmentContext.Provider value={environment}>
|
|
||||||
{children}
|
|
||||||
</EnvironmentContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { useEnvironment } from '../environments/queries/useEnvironment';
|
||||||
|
|
||||||
|
import { useEnvironmentId } from './useEnvironmentId';
|
||||||
|
|
||||||
|
export function useCurrentEnvironment() {
|
||||||
|
const id = useEnvironmentId();
|
||||||
|
return useEnvironment(id);
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { useState, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
const localStoragePrefix = 'portainer';
|
const localStoragePrefix = 'portainer';
|
||||||
|
|
||||||
function keyBuilder(key: string) {
|
export function keyBuilder(key: string) {
|
||||||
return `${localStoragePrefix}.${key}`;
|
return `${localStoragePrefix}.${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
import {
|
import {
|
||||||
PaginationTableSettings,
|
PaginationTableSettings,
|
||||||
SortableTableSettings,
|
SortableTableSettings,
|
||||||
} from '@@/datatables/types';
|
} from '@@/datatables/types-old';
|
||||||
|
|
||||||
import { useFDOProfiles } from './useFDOProfiles';
|
import { useFDOProfiles } from './useFDOProfiles';
|
||||||
import { useColumns } from './columns';
|
import { useColumns } from './columns';
|
||||||
|
|
|
@ -132,10 +132,10 @@
|
||||||
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
|
<uib-tab-heading> <i class="fa fa-pencil-alt space-right" aria-hidden="true"></i> Editor </uib-tab-heading>
|
||||||
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px" name="stackUpdateForm">
|
<form class="form-horizontal" ng-if="state.showEditorTab" style="margin-top: 10px" name="stackUpdateForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px" ng-if="stackType == 2 && composeSyntaxMaxVersion == 2">
|
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px" ng-if="stackType == STACK_TYPES.DockerCompose && composeSyntaxMaxVersion == 2">
|
||||||
This stack will be deployed using the equivalent of <code>docker compose</code>. Only Compose file format version <b>2</b> is supported at the moment.
|
This stack will be deployed using the equivalent of <code>docker compose</code>. Only Compose file format version <b>2</b> is supported at the moment.
|
||||||
</span>
|
</span>
|
||||||
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px" ng-if="stackType == 2 && composeSyntaxMaxVersion > 2">
|
<span class="col-sm-12 text-muted small" style="margin-bottom: 7px" ng-if="stackType == STACK_TYPES.DockerCompose && composeSyntaxMaxVersion > 2">
|
||||||
This stack will be deployed using <code>docker compose</code>.
|
This stack will be deployed using <code>docker compose</code>.
|
||||||
</span>
|
</span>
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
|
@ -222,11 +222,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" ng-if="containers && (!orphaned || orphanedRunning)">
|
<stack-containers-datatable
|
||||||
<div class="col-sm-12">
|
ng-if="stackType !== STACK_TYPES.DockerSwarm && (!orphaned || orphanedRunning)"
|
||||||
<containers-datatable dataset="containers" endpoint="endpoint" table-key="stack-containers"></containers-datatable>
|
stack-name="stackName"
|
||||||
</div>
|
environment="endpoint"
|
||||||
</div>
|
></stack-containers-datatable>
|
||||||
|
|
||||||
<div class="row" ng-if="services && (!orphaned || orphanedRunning)">
|
<div class="row" ng-if="services && (!orphaned || orphanedRunning)">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { ResourceControlType } from '@/portainer/access-control/types';
|
||||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||||
import { getEnvironments } from '@/portainer/environments/environment.service';
|
import { getEnvironments } from '@/portainer/environments/environment.service';
|
||||||
|
import { StackStatus, StackType } from '@/react/docker/stacks/types';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('StackController', [
|
angular.module('portainer.app').controller('StackController', [
|
||||||
'$async',
|
'$async',
|
||||||
|
@ -54,6 +55,8 @@ angular.module('portainer.app').controller('StackController', [
|
||||||
ContainerHelper,
|
ContainerHelper,
|
||||||
endpoint
|
endpoint
|
||||||
) {
|
) {
|
||||||
|
$scope.STACK_TYPES = StackType;
|
||||||
|
|
||||||
$scope.resourceType = ResourceControlType.Stack;
|
$scope.resourceType = ResourceControlType.Stack;
|
||||||
|
|
||||||
$scope.onUpdateResourceControlSuccess = function () {
|
$scope.onUpdateResourceControlSuccess = function () {
|
||||||
|
@ -363,19 +366,15 @@ angular.module('portainer.app').controller('StackController', [
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
const isSwarm = $scope.stack.Type === 1;
|
const isSwarm = $scope.stack.Type === StackType.DockerSwarm;
|
||||||
$scope.stackFileContent = data.stackFile;
|
$scope.stackFileContent = data.stackFile;
|
||||||
// workaround for missing status, if stack has resources, set the status to 1 (active), otherwise to 2 (inactive) (https://github.com/portainer/portainer/issues/4422)
|
// workaround for missing status, if stack has resources, set the status to 1 (active), otherwise to 2 (inactive) (https://github.com/portainer/portainer/issues/4422)
|
||||||
if (!$scope.stack.Status) {
|
if (!$scope.stack.Status) {
|
||||||
$scope.stack.Status = data.resources && ((isSwarm && data.resources.services.length) || data.resources.containers.length) ? 1 : 2;
|
$scope.stack.Status = data.resources && ((isSwarm && data.resources.services.length) || data.resources.containers.length) ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.stack.Status === 1) {
|
if (isSwarm && $scope.stack.Status === StackStatus.Active) {
|
||||||
if (isSwarm) {
|
|
||||||
assignSwarmStackResources(data.resources, agentProxy);
|
assignSwarmStackResources(data.resources, agentProxy);
|
||||||
} else {
|
|
||||||
assignComposeStackResources(data.resources);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
|
$scope.state.yamlError = StackHelper.validateYAML($scope.stackFileContent, $scope.containerNames);
|
||||||
|
@ -431,21 +430,15 @@ angular.module('portainer.app').controller('StackController', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignComposeStackResources(resources) {
|
|
||||||
$scope.containers = resources.containers;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadExternalStack(name) {
|
function loadExternalStack(name) {
|
||||||
var stackType = $transition$.params().type;
|
const stackType = $scope.stackType;
|
||||||
if (!stackType || (stackType !== '1' && stackType !== '2')) {
|
if (!stackType || (stackType !== StackType.DockerSwarm && stackType !== StackType.DockerCompose)) {
|
||||||
Notifications.error('Failure', null, 'Invalid type URL parameter.');
|
Notifications.error('Failure', null, 'Invalid type URL parameter.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stackType === '1') {
|
if (stackType === StackType.DockerSwarm) {
|
||||||
loadExternalSwarmStack(name);
|
loadExternalSwarmStack(name);
|
||||||
} else {
|
|
||||||
loadExternalComposeStack(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,16 +454,6 @@ angular.module('portainer.app').controller('StackController', [
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExternalComposeStack(name) {
|
|
||||||
retrieveComposeStackResources(name)
|
|
||||||
.then(function success(data) {
|
|
||||||
assignComposeStackResources(data);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve stack details');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.uiCanExit = async function () {
|
this.uiCanExit = async function () {
|
||||||
if ($scope.stackFileContent && $scope.state.isEditorDirty) {
|
if ($scope.stackFileContent && $scope.state.isEditorDirty) {
|
||||||
return ModalService.confirmWebEditorDiscard();
|
return ModalService.confirmWebEditorDiscard();
|
||||||
|
@ -515,6 +498,7 @@ angular.module('portainer.app').controller('StackController', [
|
||||||
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
|
$scope.composeSyntaxMaxVersion = endpoint.ComposeSyntaxMaxVersion;
|
||||||
|
|
||||||
$scope.stackType = $transition$.params().type;
|
$scope.stackType = $transition$.params().type;
|
||||||
|
$scope.editorReadOnly = !Authentication.hasAuthorizations(['PortainerStackUpdate']);
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
PaginationTableSettings,
|
PaginationTableSettings,
|
||||||
SortableTableSettings,
|
SortableTableSettings,
|
||||||
} from '@@/datatables/types';
|
} from '@@/datatables/types-old';
|
||||||
|
|
||||||
export interface TableSettings
|
export interface TableSettings
|
||||||
extends PaginationTableSettings,
|
extends PaginationTableSettings,
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
import {
|
||||||
|
useTable,
|
||||||
|
useFilters,
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy,
|
||||||
|
usePagination,
|
||||||
|
Column,
|
||||||
|
Row,
|
||||||
|
TableInstance,
|
||||||
|
TableState,
|
||||||
|
} from 'react-table';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||||
|
|
||||||
|
import { PaginationControls } from '@@/PaginationControls';
|
||||||
|
|
||||||
|
import { Table } from './Table';
|
||||||
|
import { multiple } from './filter-types';
|
||||||
|
import { SearchBar, useSearchBarState } from './SearchBar';
|
||||||
|
import { SelectedRowsCount } from './SelectedRowsCount';
|
||||||
|
import { TableSettingsProvider } from './useZustandTableSettings';
|
||||||
|
import { useRowSelect } from './useRowSelect';
|
||||||
|
import { PaginationTableSettings, SortableTableSettings } from './types';
|
||||||
|
|
||||||
|
interface DefaultTableSettings
|
||||||
|
extends SortableTableSettings,
|
||||||
|
PaginationTableSettings {}
|
||||||
|
|
||||||
|
interface TitleOptionsVisible {
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
hide?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TitleOptions = TitleOptionsVisible | { hide: true };
|
||||||
|
|
||||||
|
interface Props<
|
||||||
|
D extends Record<string, unknown>,
|
||||||
|
TSettings extends DefaultTableSettings
|
||||||
|
> {
|
||||||
|
dataset: D[];
|
||||||
|
storageKey: string;
|
||||||
|
columns: readonly Column<D>[];
|
||||||
|
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
||||||
|
renderTableActions?(selectedRows: D[]): ReactNode;
|
||||||
|
settingsStore: TSettings;
|
||||||
|
disableSelect?: boolean;
|
||||||
|
getRowId?(row: D): string;
|
||||||
|
isRowSelectable?(row: Row<D>): boolean;
|
||||||
|
emptyContentLabel?: string;
|
||||||
|
titleOptions: TitleOptions;
|
||||||
|
initialTableState?: Partial<TableState<D>>;
|
||||||
|
isLoading?: boolean;
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Datatable<
|
||||||
|
D extends Record<string, unknown>,
|
||||||
|
TSettings extends DefaultTableSettings
|
||||||
|
>({
|
||||||
|
columns,
|
||||||
|
dataset,
|
||||||
|
storageKey,
|
||||||
|
renderTableSettings,
|
||||||
|
renderTableActions,
|
||||||
|
settingsStore,
|
||||||
|
disableSelect,
|
||||||
|
getRowId = defaultGetRowId,
|
||||||
|
isRowSelectable = () => true,
|
||||||
|
titleOptions,
|
||||||
|
emptyContentLabel,
|
||||||
|
initialTableState = {},
|
||||||
|
isLoading,
|
||||||
|
totalCount = dataset.length,
|
||||||
|
}: Props<D, TSettings>) {
|
||||||
|
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||||
|
|
||||||
|
const tableInstance = useTable<D>(
|
||||||
|
{
|
||||||
|
defaultCanFilter: false,
|
||||||
|
columns,
|
||||||
|
data: dataset,
|
||||||
|
filterTypes: { multiple },
|
||||||
|
initialState: {
|
||||||
|
pageSize: settingsStore.pageSize || 10,
|
||||||
|
sortBy: [settingsStore.sortBy],
|
||||||
|
globalFilter: searchBarValue,
|
||||||
|
...initialTableState,
|
||||||
|
},
|
||||||
|
isRowSelectable,
|
||||||
|
autoResetSelectedRows: false,
|
||||||
|
getRowId,
|
||||||
|
stateReducer: (newState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'setGlobalFilter':
|
||||||
|
setSearchBarValue(action.filterValue);
|
||||||
|
break;
|
||||||
|
case 'toggleSortBy':
|
||||||
|
settingsStore.setSortBy(action.columnId, action.desc);
|
||||||
|
break;
|
||||||
|
case 'setPageSize':
|
||||||
|
settingsStore.setPageSize(action.pageSize);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useFilters,
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy,
|
||||||
|
usePagination,
|
||||||
|
useRowSelect,
|
||||||
|
!disableSelect ? useRowSelectColumn : emptyPlugin
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedFlatRows,
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
page,
|
||||||
|
prepareRow,
|
||||||
|
gotoPage,
|
||||||
|
setPageSize,
|
||||||
|
setGlobalFilter,
|
||||||
|
state: { pageIndex, pageSize },
|
||||||
|
} = tableInstance;
|
||||||
|
|
||||||
|
const tableProps = getTableProps();
|
||||||
|
const tbodyProps = getTableBodyProps();
|
||||||
|
|
||||||
|
const selectedItems = selectedFlatRows.map((row) => row.original);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<TableSettingsProvider settings={settingsStore}>
|
||||||
|
<Table.Container>
|
||||||
|
{isTitleVisible(titleOptions) && (
|
||||||
|
<Table.Title label={titleOptions.title} icon={titleOptions.icon}>
|
||||||
|
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
||||||
|
{renderTableActions && (
|
||||||
|
<Table.Actions>
|
||||||
|
{renderTableActions(selectedItems)}
|
||||||
|
</Table.Actions>
|
||||||
|
)}
|
||||||
|
<Table.TitleActions>
|
||||||
|
{!!renderTableSettings && renderTableSettings(tableInstance)}
|
||||||
|
</Table.TitleActions>
|
||||||
|
</Table.Title>
|
||||||
|
)}
|
||||||
|
<Table
|
||||||
|
className={tableProps.className}
|
||||||
|
role={tableProps.role}
|
||||||
|
style={tableProps.style}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
{headerGroups.map((headerGroup) => {
|
||||||
|
const { key, className, role, style } =
|
||||||
|
headerGroup.getHeaderGroupProps();
|
||||||
|
return (
|
||||||
|
<Table.HeaderRow<D>
|
||||||
|
key={key}
|
||||||
|
className={className}
|
||||||
|
role={role}
|
||||||
|
style={style}
|
||||||
|
headers={headerGroup.headers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
className={tbodyProps.className}
|
||||||
|
role={tbodyProps.role}
|
||||||
|
style={tbodyProps.style}
|
||||||
|
>
|
||||||
|
<Table.Content<D>
|
||||||
|
rows={page}
|
||||||
|
isLoading={isLoading}
|
||||||
|
prepareRow={prepareRow}
|
||||||
|
emptyContent={emptyContentLabel}
|
||||||
|
renderRow={(row, { key, className, role, style }) => (
|
||||||
|
<Table.Row<D>
|
||||||
|
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={setPageSize}
|
||||||
|
/>
|
||||||
|
</Table.Footer>
|
||||||
|
</Table.Container>
|
||||||
|
</TableSettingsProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTitleVisible(
|
||||||
|
titleSettings: TitleOptions
|
||||||
|
): titleSettings is TitleOptionsVisible {
|
||||||
|
return !titleSettings.hide;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultGetRowId<D extends Record<string, unknown>>(row: D): string {
|
||||||
|
if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) {
|
||||||
|
return row.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.Id && (typeof row.Id === 'string' || typeof row.Id === 'number')) {
|
||||||
|
return row.Id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) {
|
||||||
|
return row.ID.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyPlugin() {}
|
||||||
|
|
||||||
|
emptyPlugin.pluginName = 'emptyPlugin';
|
|
@ -1,9 +1,14 @@
|
||||||
|
import {
|
||||||
|
SettableQuickActionsTableSettings,
|
||||||
|
QuickAction,
|
||||||
|
} from '@/react/docker/containers/ListView/ContainersDatatable/types';
|
||||||
|
|
||||||
import { Checkbox } from '@@/form-components/Checkbox';
|
import { Checkbox } from '@@/form-components/Checkbox';
|
||||||
|
|
||||||
import { useTableSettings } from './useTableSettings';
|
import { useTableSettings } from './useZustandTableSettings';
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
id: string;
|
id: QuickAction;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,13 +16,9 @@ interface Props {
|
||||||
actions: Action[];
|
actions: Action[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuickActionsSettingsType {
|
|
||||||
hiddenQuickActions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuickActionsSettings({ actions }: Props) {
|
export function QuickActionsSettings({ actions }: Props) {
|
||||||
const { settings, setTableSettings } =
|
const { settings } =
|
||||||
useTableSettings<QuickActionsSettingsType>();
|
useTableSettings<SettableQuickActionsTableSettings<QuickAction>>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -33,16 +34,17 @@ export function QuickActionsSettings({ actions }: Props) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function toggleAction(key: string, value: boolean) {
|
function toggleAction(key: QuickAction, visible: boolean) {
|
||||||
setTableSettings(({ hiddenQuickActions = [], ...settings }) => ({
|
if (!visible) {
|
||||||
...settings,
|
settings.setHiddenQuickActions([...settings.hiddenQuickActions, key]);
|
||||||
hiddenQuickActions: value
|
} else {
|
||||||
? hiddenQuickActions.filter((id) => id !== key)
|
settings.setHiddenQuickActions(
|
||||||
: [...hiddenQuickActions, key],
|
settings.hiddenQuickActions.filter((action) => action !== key)
|
||||||
}));
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAction(id: string, label: string): Action {
|
export function buildAction(id: QuickAction, label: string): Action {
|
||||||
return { id, label };
|
return { id, label };
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||||
|
|
||||||
|
export function createRowContext<TContext>() {
|
||||||
|
const Context = createContext<TContext | null>(null);
|
||||||
|
|
||||||
|
return { RowProvider, useRowContext };
|
||||||
|
|
||||||
|
function RowProvider({
|
||||||
|
children,
|
||||||
|
context,
|
||||||
|
}: PropsWithChildren<{ context: TContext }>) {
|
||||||
|
return <Context.Provider value={context}>{children}</Context.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRowContext() {
|
||||||
|
const context = useContext(Context);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('should be nested under RowProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,18 @@ import clsx from 'clsx';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { TableProps } from 'react-table';
|
import { TableProps } from 'react-table';
|
||||||
|
|
||||||
import { useTableContext } from './TableContainer';
|
import { useTableContext, TableContainer } from './TableContainer';
|
||||||
|
import { TableActions } from './TableActions';
|
||||||
|
import { TableTitleActions } from './TableTitleActions';
|
||||||
|
import { TableHeaderCell } from './TableHeaderCell';
|
||||||
|
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||||
|
import { TableTitle } from './TableTitle';
|
||||||
|
import { TableHeaderRow } from './TableHeaderRow';
|
||||||
|
import { TableRow } from './TableRow';
|
||||||
|
import { TableContent } from './TableContent';
|
||||||
|
import { TableFooter } from './TableFooter';
|
||||||
|
|
||||||
export function Table({
|
function MainComponent({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
role,
|
role,
|
||||||
|
@ -27,3 +36,30 @@ export function Table({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Cell, TableRowProps } from 'react-table';
|
||||||
import { useTableContext } from './TableContainer';
|
import { useTableContext } from './TableContainer';
|
||||||
|
|
||||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>>
|
interface Props<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
extends TableRowProps {
|
extends Omit<TableRowProps, 'key'> {
|
||||||
cells: Cell<D>[];
|
cells: Cell<D>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
import { Icon, IconProps } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
import { useTableContext } from './TableContainer';
|
import { useTableContext } from './TableContainer';
|
||||||
|
|
||||||
interface Props extends IconProps {
|
interface Props {
|
||||||
|
icon?: ReactNode | ComponentType<unknown>;
|
||||||
|
featherIcon?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +21,7 @@ export function TableTitle({
|
||||||
return (
|
return (
|
||||||
<div className="toolBar">
|
<div className="toolBar">
|
||||||
<div className="toolBarTitle">
|
<div className="toolBarTitle">
|
||||||
|
{icon && (
|
||||||
<div className="widget-icon">
|
<div className="widget-icon">
|
||||||
<Icon
|
<Icon
|
||||||
icon={icon}
|
icon={icon}
|
||||||
|
@ -26,6 +29,7 @@ export function TableTitle({
|
||||||
className="space-right feather"
|
className="space-right feather"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export { Datatable } from './Datatable';
|
||||||
|
|
||||||
|
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';
|
||||||
|
export { TableContent } from './TableContent';
|
||||||
|
export { TableFooter } from './TableFooter';
|
|
@ -1,50 +0,0 @@
|
||||||
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,15 @@
|
||||||
|
export interface PaginationTableSettings {
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SortableTableSettings {
|
||||||
|
sortBy: { id: string; desc: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettableColumnsTableSettings {
|
||||||
|
hiddenColumns: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshableTableSettings {
|
||||||
|
autoRefreshRate: number;
|
||||||
|
}
|
|
@ -1,19 +1,61 @@
|
||||||
export interface PaginationTableSettings {
|
export interface PaginationTableSettings {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
setPageSize: (pageSize: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Set<T> = (
|
||||||
|
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||||
|
replace?: boolean | undefined
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export function paginationSettings(
|
||||||
|
set: Set<PaginationTableSettings>
|
||||||
|
): PaginationTableSettings {
|
||||||
|
return {
|
||||||
|
pageSize: 10,
|
||||||
|
setPageSize: (pageSize: number) => set({ pageSize }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SortableTableSettings {
|
export interface SortableTableSettings {
|
||||||
sortBy: { id: string; desc: boolean };
|
sortBy: { id: string; desc: boolean };
|
||||||
|
setSortBy: (id: string, desc: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortableSettings(
|
||||||
|
set: Set<SortableTableSettings>,
|
||||||
|
initialSortBy = 'name'
|
||||||
|
): SortableTableSettings {
|
||||||
|
return {
|
||||||
|
sortBy: { id: initialSortBy, desc: false },
|
||||||
|
setSortBy: (id: string, desc: boolean) => set({ sortBy: { id, desc } }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettableColumnsTableSettings {
|
export interface SettableColumnsTableSettings {
|
||||||
hiddenColumns: string[];
|
hiddenColumns: string[];
|
||||||
|
setHiddenColumns: (hiddenColumns: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettableQuickActionsTableSettings<TAction> {
|
export function hiddenColumnsSettings(
|
||||||
hiddenQuickActions: TAction[];
|
set: Set<SettableColumnsTableSettings>
|
||||||
|
): SettableColumnsTableSettings {
|
||||||
|
return {
|
||||||
|
hiddenColumns: [],
|
||||||
|
setHiddenColumns: (hiddenColumns: string[]) => set({ hiddenColumns }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshableTableSettings {
|
export interface RefreshableTableSettings {
|
||||||
autoRefreshRate: number;
|
autoRefreshRate: number;
|
||||||
|
setAutoRefreshRate: (autoRefreshRate: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshableSettings(
|
||||||
|
set: Set<RefreshableTableSettings>
|
||||||
|
): RefreshableTableSettings {
|
||||||
|
return {
|
||||||
|
autoRefreshRate: 0,
|
||||||
|
setAutoRefreshRate: (autoRefreshRate: number) => set({ autoRefreshRate }),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
|
|
||||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||||
|
|
||||||
export interface TableSettingsContextInterface<T> {
|
interface TableSettingsContextInterface<T> {
|
||||||
settings: T;
|
settings: T;
|
||||||
setTableSettings(partialSettings: Partial<T>): void;
|
setTableSettings(partialSettings: Partial<T>): void;
|
||||||
setTableSettings(mutation: (settings: T) => T): void;
|
setTableSettings(mutation: (settings: T) => T): void;
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Context, createContext, ReactNode, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
interface TableSettingsContextInterface<T> {
|
||||||
|
settings: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TableSettingsContext = createContext<TableSettingsContextInterface<
|
||||||
|
Record<string, unknown>
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
export function useTableSettings<T>() {
|
||||||
|
const Context = getContextType<T>();
|
||||||
|
|
||||||
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error('must be nested under TableSettingsProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderProps<T> {
|
||||||
|
children: ReactNode;
|
||||||
|
settings: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TableSettingsProvider<T>({
|
||||||
|
children,
|
||||||
|
settings,
|
||||||
|
}: ProviderProps<T>) {
|
||||||
|
const Context = getContextType<T>();
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
settings,
|
||||||
|
}),
|
||||||
|
[settings]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContextType<T>() {
|
||||||
|
return TableSettingsContext as unknown as Context<
|
||||||
|
TableSettingsContextInterface<T>
|
||||||
|
>;
|
||||||
|
}
|
|
@ -1,256 +1,105 @@
|
||||||
import { useEffect } from 'react';
|
import _ from 'lodash';
|
||||||
import {
|
|
||||||
useTable,
|
|
||||||
useSortBy,
|
|
||||||
useFilters,
|
|
||||||
useGlobalFilter,
|
|
||||||
usePagination,
|
|
||||||
Row,
|
|
||||||
} from 'react-table';
|
|
||||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
|
||||||
|
|
||||||
import { useDebounce } from '@/portainer/hooks/useDebounce';
|
import { Environment } from '@/portainer/environments/types';
|
||||||
import type {
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
ContainersTableSettings,
|
|
||||||
DockerContainer,
|
|
||||||
} from '@/react/docker/containers/types';
|
|
||||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
|
||||||
|
|
||||||
import { PaginationControls } from '@@/PaginationControls';
|
import { TableSettingsMenu, Datatable } from '@@/datatables';
|
||||||
import {
|
import {
|
||||||
QuickActionsSettings,
|
|
||||||
buildAction,
|
buildAction,
|
||||||
|
QuickActionsSettings,
|
||||||
} from '@@/datatables/QuickActionsSettings';
|
} from '@@/datatables/QuickActionsSettings';
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableActions,
|
|
||||||
TableContainer,
|
|
||||||
TableHeaderRow,
|
|
||||||
TableRow,
|
|
||||||
TableSettingsMenu,
|
|
||||||
TableTitle,
|
|
||||||
TableTitleActions,
|
|
||||||
} from '@@/datatables';
|
|
||||||
import { multiple } from '@@/datatables/filter-types';
|
|
||||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
|
||||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||||
import { useRepeater } from '@@/datatables/useRepeater';
|
|
||||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
|
||||||
import { Checkbox } from '@@/form-components/Checkbox';
|
|
||||||
import { TableFooter } from '@@/datatables/TableFooter';
|
|
||||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
|
||||||
|
|
||||||
import { ContainersDatatableActions } from './ContainersDatatableActions';
|
import { useContainers } from '../../queries/containers';
|
||||||
|
|
||||||
|
import { createStore } from './datatable-store';
|
||||||
import { ContainersDatatableSettings } from './ContainersDatatableSettings';
|
import { ContainersDatatableSettings } from './ContainersDatatableSettings';
|
||||||
import { useColumns } from './columns';
|
import { useColumns } from './columns';
|
||||||
|
import { ContainersDatatableActions } from './ContainersDatatableActions';
|
||||||
|
import { RowProvider } from './RowContext';
|
||||||
|
|
||||||
export interface ContainerTableProps {
|
const storageKey = 'containers';
|
||||||
isAddActionVisible: boolean;
|
const useStore = createStore(storageKey);
|
||||||
dataset: DockerContainer[];
|
|
||||||
onRefresh?(): Promise<void>;
|
|
||||||
isHostColumnVisible: boolean;
|
|
||||||
tableKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContainersDatatable({
|
const actions = [
|
||||||
isAddActionVisible,
|
|
||||||
dataset,
|
|
||||||
onRefresh,
|
|
||||||
isHostColumnVisible,
|
|
||||||
}: ContainerTableProps) {
|
|
||||||
const { settings, setTableSettings } =
|
|
||||||
useTableSettings<ContainersTableSettings>();
|
|
||||||
const [searchBarValue, setSearchBarValue] = useSearchBarState('containers');
|
|
||||||
|
|
||||||
const columns = useColumns();
|
|
||||||
|
|
||||||
const endpoint = useEnvironment();
|
|
||||||
|
|
||||||
useRepeater(settings.autoRefreshRate, onRefresh);
|
|
||||||
|
|
||||||
const {
|
|
||||||
getTableProps,
|
|
||||||
getTableBodyProps,
|
|
||||||
headerGroups,
|
|
||||||
page,
|
|
||||||
prepareRow,
|
|
||||||
selectedFlatRows,
|
|
||||||
allColumns,
|
|
||||||
gotoPage,
|
|
||||||
setPageSize,
|
|
||||||
setHiddenColumns,
|
|
||||||
toggleHideColumn,
|
|
||||||
setGlobalFilter,
|
|
||||||
state: { pageIndex, pageSize },
|
|
||||||
} = useTable<DockerContainer>(
|
|
||||||
{
|
|
||||||
defaultCanFilter: false,
|
|
||||||
columns,
|
|
||||||
data: dataset,
|
|
||||||
filterTypes: { multiple },
|
|
||||||
initialState: {
|
|
||||||
pageSize: settings.pageSize || 10,
|
|
||||||
hiddenColumns: settings.hiddenColumns,
|
|
||||||
sortBy: [settings.sortBy],
|
|
||||||
globalFilter: searchBarValue,
|
|
||||||
},
|
|
||||||
isRowSelectable(row: Row<DockerContainer>) {
|
|
||||||
return !row.original.IsPortainer;
|
|
||||||
},
|
|
||||||
autoResetSelectedRows: false,
|
|
||||||
getRowId(originalRow: DockerContainer) {
|
|
||||||
return originalRow.Id;
|
|
||||||
},
|
|
||||||
selectCheckboxComponent: Checkbox,
|
|
||||||
},
|
|
||||||
useFilters,
|
|
||||||
useGlobalFilter,
|
|
||||||
useSortBy,
|
|
||||||
usePagination,
|
|
||||||
useRowSelect,
|
|
||||||
useRowSelectColumn
|
|
||||||
);
|
|
||||||
|
|
||||||
const debouncedSearchValue = useDebounce(searchBarValue);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setGlobalFilter(debouncedSearchValue);
|
|
||||||
}, [debouncedSearchValue, setGlobalFilter]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
toggleHideColumn('host', !isHostColumnVisible);
|
|
||||||
}, [toggleHideColumn, isHostColumnVisible]);
|
|
||||||
|
|
||||||
const columnsToHide = allColumns.filter((colInstance) => {
|
|
||||||
const columnDef = columns.find((c) => c.id === colInstance.id);
|
|
||||||
return columnDef?.canHide;
|
|
||||||
});
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
buildAction('logs', 'Logs'),
|
buildAction('logs', 'Logs'),
|
||||||
buildAction('inspect', 'Inspect'),
|
buildAction('inspect', 'Inspect'),
|
||||||
buildAction('stats', 'Stats'),
|
buildAction('stats', 'Stats'),
|
||||||
buildAction('exec', 'Console'),
|
buildAction('exec', 'Console'),
|
||||||
buildAction('attach', 'Attach'),
|
buildAction('attach', 'Attach'),
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableProps = getTableProps();
|
export interface Props {
|
||||||
const tbodyProps = getTableBodyProps();
|
isHostColumnVisible: boolean;
|
||||||
|
environment: Environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContainersDatatable({
|
||||||
|
isHostColumnVisible,
|
||||||
|
environment,
|
||||||
|
}: Props) {
|
||||||
|
const settings = useStore();
|
||||||
|
const columns = useColumns(isHostColumnVisible);
|
||||||
|
const hidableColumns = _.compact(
|
||||||
|
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const containersQuery = useContainers(
|
||||||
|
environment.Id,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
settings.autoRefreshRate * 1000
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<RowProvider context={{ environment }}>
|
||||||
<TableTitle icon="box" featherIcon label="Containers">
|
<Datatable
|
||||||
<SearchBar
|
titleOptions={{
|
||||||
value={searchBarValue}
|
icon: 'fa-cubes',
|
||||||
onChange={handleSearchBarChange}
|
title: 'Containers',
|
||||||
placeholder="Search for a container..."
|
}}
|
||||||
/>
|
settingsStore={settings}
|
||||||
<TableActions>
|
columns={columns}
|
||||||
|
renderTableActions={(selectedRows) => (
|
||||||
<ContainersDatatableActions
|
<ContainersDatatableActions
|
||||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
selectedItems={selectedRows}
|
||||||
isAddActionVisible={isAddActionVisible}
|
isAddActionVisible
|
||||||
endpointId={endpoint.Id}
|
endpointId={environment.Id}
|
||||||
/>
|
/>
|
||||||
</TableActions>
|
)}
|
||||||
<TableTitleActions>
|
isLoading={containersQuery.isLoading}
|
||||||
|
isRowSelectable={(row) => !row.original.IsPortainer}
|
||||||
|
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||||
|
renderTableSettings={(tableInstance) => {
|
||||||
|
const columnsToHide = tableInstance.allColumns.filter((colInstance) =>
|
||||||
|
hidableColumns?.includes(colInstance.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
<ColumnVisibilityMenu<DockerContainer>
|
<ColumnVisibilityMenu<DockerContainer>
|
||||||
columns={columnsToHide}
|
columns={columnsToHide}
|
||||||
onChange={handleChangeColumnsVisibility}
|
onChange={(hiddenColumns) => {
|
||||||
|
settings.setHiddenColumns(hiddenColumns);
|
||||||
|
tableInstance.setHiddenColumns(hiddenColumns);
|
||||||
|
}}
|
||||||
value={settings.hiddenColumns}
|
value={settings.hiddenColumns}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableSettingsMenu
|
<TableSettingsMenu
|
||||||
quickActions={<QuickActionsSettings actions={actions} />}
|
quickActions={<QuickActionsSettings actions={actions} />}
|
||||||
>
|
>
|
||||||
<ContainersDatatableSettings isRefreshVisible={!!onRefresh} />
|
<ContainersDatatableSettings
|
||||||
|
isRefreshVisible
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
</TableSettingsMenu>
|
</TableSettingsMenu>
|
||||||
</TableTitleActions>
|
</>
|
||||||
</TableTitle>
|
|
||||||
|
|
||||||
<Table
|
|
||||||
className={tableProps.className}
|
|
||||||
role={tableProps.role}
|
|
||||||
style={tableProps.style}
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
{headerGroups.map((headerGroup) => {
|
|
||||||
const { key, className, role, style } =
|
|
||||||
headerGroup.getHeaderGroupProps();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableHeaderRow<DockerContainer>
|
|
||||||
key={key}
|
|
||||||
className={className}
|
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
headers={headerGroup.headers}
|
|
||||||
onSortChange={handleSortChange}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
}}
|
||||||
</thead>
|
storageKey={storageKey}
|
||||||
<tbody
|
dataset={containersQuery.data || []}
|
||||||
className={tbodyProps.className}
|
emptyContentLabel="No containers found"
|
||||||
role={tbodyProps.role}
|
|
||||||
style={tbodyProps.style}
|
|
||||||
>
|
|
||||||
{page.length > 0 ? (
|
|
||||||
page.map((row) => {
|
|
||||||
prepareRow(row);
|
|
||||||
const { key, className, role, style } = row.getRowProps();
|
|
||||||
return (
|
|
||||||
<TableRow<DockerContainer>
|
|
||||||
cells={row.cells}
|
|
||||||
key={key}
|
|
||||||
className={className}
|
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
/>
|
/>
|
||||||
|
</RowProvider>
|
||||||
);
|
);
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={columns.length} className="text-center text-muted">
|
|
||||||
No container available.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TableFooter>
|
|
||||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
|
||||||
<PaginationControls
|
|
||||||
showAll
|
|
||||||
pageLimit={pageSize}
|
|
||||||
page={pageIndex + 1}
|
|
||||||
onPageChange={(p) => gotoPage(p - 1)}
|
|
||||||
totalCount={dataset.length}
|
|
||||||
onPageLimitChange={handlePageSizeChange}
|
|
||||||
/>
|
|
||||||
</TableFooter>
|
|
||||||
</TableContainer>
|
|
||||||
);
|
|
||||||
|
|
||||||
function handlePageSizeChange(pageSize: number) {
|
|
||||||
setPageSize(pageSize);
|
|
||||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChangeColumnsVisibility(hiddenColumns: string[]) {
|
|
||||||
setHiddenColumns(hiddenColumns);
|
|
||||||
setTableSettings((settings) => ({ ...settings, hiddenColumns }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearchBarChange(value: string) {
|
|
||||||
setSearchBarValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSortChange(id: string, desc: boolean) {
|
|
||||||
setTableSettings((settings) => ({
|
|
||||||
...settings,
|
|
||||||
sortBy: { id, desc },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ import * as notifications from '@/portainer/services/notifications';
|
||||||
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
|
import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser';
|
||||||
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
|
import { confirmContainerDeletion } from '@/portainer/services/modal.service/prompt';
|
||||||
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
|
import { setPortainerAgentTargetHeader } from '@/portainer/services/http-request.helper';
|
||||||
import type {
|
import {
|
||||||
ContainerId,
|
ContainerId,
|
||||||
|
ContainerStatus,
|
||||||
DockerContainer,
|
DockerContainer,
|
||||||
} from '@/react/docker/containers/types';
|
} from '@/react/docker/containers/types';
|
||||||
import {
|
import {
|
||||||
|
@ -40,13 +41,22 @@ export function ContainersDatatableActions({
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const selectedItemCount = selectedItems.length;
|
const selectedItemCount = selectedItems.length;
|
||||||
const hasPausedItemsSelected = selectedItems.some(
|
const hasPausedItemsSelected = selectedItems.some(
|
||||||
(item) => item.Status === 'paused'
|
(item) => item.State === ContainerStatus.Paused
|
||||||
);
|
);
|
||||||
const hasStoppedItemsSelected = selectedItems.some((item) =>
|
const hasStoppedItemsSelected = selectedItems.some((item) =>
|
||||||
['stopped', 'created'].includes(item.Status)
|
[
|
||||||
|
ContainerStatus.Stopped,
|
||||||
|
ContainerStatus.Created,
|
||||||
|
ContainerStatus.Exited,
|
||||||
|
].includes(item.Status)
|
||||||
);
|
);
|
||||||
const hasRunningItemsSelected = selectedItems.some((item) =>
|
const hasRunningItemsSelected = selectedItems.some((item) =>
|
||||||
['running', 'healthy', 'unhealthy', 'starting'].includes(item.Status)
|
[
|
||||||
|
ContainerStatus.Running,
|
||||||
|
ContainerStatus.Healthy,
|
||||||
|
ContainerStatus.Unhealthy,
|
||||||
|
ContainerStatus.Starting,
|
||||||
|
].includes(item.Status)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAuthorized = useAuthorizations([
|
const isAuthorized = useAuthorizations([
|
||||||
|
@ -95,7 +105,7 @@ export function ContainersDatatableActions({
|
||||||
<Button
|
<Button
|
||||||
color="light"
|
color="light"
|
||||||
onClick={() => onKillClick(selectedItems)}
|
onClick={() => onKillClick(selectedItems)}
|
||||||
disabled={selectedItemCount === 0}
|
disabled={selectedItemCount === 0 || hasStoppedItemsSelected}
|
||||||
>
|
>
|
||||||
<i className="fa fa-bomb space-right" aria-hidden="true" />
|
<i className="fa fa-bomb space-right" aria-hidden="true" />
|
||||||
Kill
|
Kill
|
||||||
|
@ -228,7 +238,7 @@ export function ContainersDatatableActions({
|
||||||
|
|
||||||
function onRemoveClick(selectedItems: DockerContainer[]) {
|
function onRemoveClick(selectedItems: DockerContainer[]) {
|
||||||
const isOneContainerRunning = selectedItems.some(
|
const isOneContainerRunning = selectedItems.some(
|
||||||
(container) => container.Status === 'running'
|
(container) => container.State === 'running'
|
||||||
);
|
);
|
||||||
|
|
||||||
const runningTitle = isOneContainerRunning ? 'running' : '';
|
const runningTitle = isOneContainerRunning ? 'running' : '';
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { EnvironmentProvider } from '@/portainer/environments/useEnvironment';
|
|
||||||
import type { Environment } from '@/portainer/environments/types';
|
|
||||||
|
|
||||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ContainersDatatable,
|
|
||||||
ContainerTableProps,
|
|
||||||
} from './ContainersDatatable';
|
|
||||||
|
|
||||||
interface Props extends ContainerTableProps {
|
|
||||||
endpoint: Environment;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ContainersDatatableContainer({
|
|
||||||
endpoint,
|
|
||||||
tableKey = 'containers',
|
|
||||||
...props
|
|
||||||
}: Props) {
|
|
||||||
const defaultSettings = {
|
|
||||||
autoRefreshRate: 0,
|
|
||||||
truncateContainerName: 32,
|
|
||||||
hiddenQuickActions: [],
|
|
||||||
hiddenColumns: [],
|
|
||||||
pageSize: 10,
|
|
||||||
sortBy: { id: 'state', desc: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnvironmentProvider environment={endpoint}>
|
|
||||||
<TableSettingsProvider defaults={defaultSettings} storageKey={tableKey}>
|
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<ContainersDatatable {...props} />
|
|
||||||
</TableSettingsProvider>
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +1,18 @@
|
||||||
import type { ContainersTableSettings } from '@/react/docker/containers/types';
|
|
||||||
|
|
||||||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
|
||||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
|
||||||
import { Checkbox } from '@@/form-components/Checkbox';
|
import { Checkbox } from '@@/form-components/Checkbox';
|
||||||
|
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||||
|
|
||||||
|
import { TableSettings } from './types';
|
||||||
|
import { TRUNCATE_LENGTH } from './datatable-store';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isRefreshVisible: boolean;
|
isRefreshVisible?: boolean;
|
||||||
|
settings: TableSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContainersDatatableSettings({ isRefreshVisible }: Props) {
|
export function ContainersDatatableSettings({
|
||||||
const { settings, setTableSettings } =
|
isRefreshVisible,
|
||||||
useTableSettings<ContainersTableSettings>();
|
settings,
|
||||||
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -19,23 +20,18 @@ export function ContainersDatatableSettings({ isRefreshVisible }: Props) {
|
||||||
label="Truncate container name"
|
label="Truncate container name"
|
||||||
checked={settings.truncateContainerName > 0}
|
checked={settings.truncateContainerName > 0}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
setTableSettings((settings) => ({
|
settings.setTruncateContainerName(
|
||||||
...settings,
|
settings.truncateContainerName > 0 ? 0 : TRUNCATE_LENGTH
|
||||||
truncateContainerName: settings.truncateContainerName > 0 ? 0 : 32,
|
)
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isRefreshVisible && (
|
{isRefreshVisible && (
|
||||||
<TableSettingsMenuAutoRefresh
|
<TableSettingsMenuAutoRefresh
|
||||||
value={settings.autoRefreshRate}
|
value={settings.autoRefreshRate}
|
||||||
onChange={handleRefreshRateChange}
|
onChange={(value) => settings.setAutoRefreshRate(value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleRefreshRateChange(autoRefreshRate: number) {
|
|
||||||
setTableSettings({ autoRefreshRate });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { createRowContext } from '@@/datatables/RowContext';
|
||||||
|
|
||||||
|
interface RowContextState {
|
||||||
|
environment: Environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||||
|
|
||||||
|
export { RowProvider, useRowContext };
|
|
@ -1,12 +1,28 @@
|
||||||
import { Column } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId';
|
||||||
|
import { useContainerGpus } from '@/react/docker/containers/queries/gpus';
|
||||||
|
|
||||||
export const gpus: Column<DockerContainer> = {
|
export const gpus: Column<DockerContainer> = {
|
||||||
Header: 'GPUs',
|
Header: 'GPUs',
|
||||||
accessor: 'Gpus',
|
|
||||||
id: 'gpus',
|
id: 'gpus',
|
||||||
disableFilters: true,
|
disableFilters: true,
|
||||||
canHide: true,
|
canHide: true,
|
||||||
Filter: () => null,
|
Filter: () => null,
|
||||||
|
Cell: GpusCell,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function GpusCell({
|
||||||
|
row: { original: container },
|
||||||
|
}: CellProps<DockerContainer>) {
|
||||||
|
const containerId = container.Id;
|
||||||
|
const environmentId = useEnvironmentId();
|
||||||
|
const gpusQuery = useContainerGpus(environmentId, containerId);
|
||||||
|
|
||||||
|
if (!gpusQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{gpusQuery.data}</>;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Column } from 'react-table';
|
import { Column } from 'react-table';
|
||||||
import { useSref } from '@uirouter/react';
|
import { useSref } from '@uirouter/react';
|
||||||
|
|
||||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
|
||||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||||
|
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||||
|
|
||||||
export const image: Column<DockerContainer> = {
|
export const image: Column<DockerContainer> = {
|
||||||
Header: 'Image',
|
Header: 'Image',
|
||||||
|
@ -21,14 +21,15 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageCell({ value: imageName }: Props) {
|
function ImageCell({ value: imageName }: Props) {
|
||||||
const endpoint = useEnvironment();
|
const linkProps = useSref('docker.images.image', { id: imageName });
|
||||||
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
|
|
||||||
|
|
||||||
const shortImageName = trimSHASum(imageName);
|
const shortImageName = trimSHASum(imageName);
|
||||||
|
|
||||||
const linkProps = useSref('docker.images.image', { id: imageName });
|
const environmentQuery = useCurrentEnvironment();
|
||||||
if (offlineMode) {
|
|
||||||
return shortImageName;
|
const environment = environmentQuery.data;
|
||||||
|
|
||||||
|
if (!environment || isOfflineEndpoint(environment)) {
|
||||||
|
return <span>{shortImageName}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { created } from './created';
|
import { created } from './created';
|
||||||
|
@ -12,9 +13,10 @@ import { stack } from './stack';
|
||||||
import { state } from './state';
|
import { state } from './state';
|
||||||
import { gpus } from './gpus';
|
import { gpus } from './gpus';
|
||||||
|
|
||||||
export function useColumns() {
|
export function useColumns(isHostColumnVisible: boolean) {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() =>
|
||||||
|
_.compact([
|
||||||
name,
|
name,
|
||||||
state,
|
state,
|
||||||
quickActions,
|
quickActions,
|
||||||
|
@ -22,11 +24,11 @@ export function useColumns() {
|
||||||
image,
|
image,
|
||||||
created,
|
created,
|
||||||
ip,
|
ip,
|
||||||
host,
|
isHostColumnVisible && host,
|
||||||
gpus,
|
gpus,
|
||||||
ports,
|
ports,
|
||||||
ownership,
|
ownership,
|
||||||
],
|
]),
|
||||||
[]
|
[isHostColumnVisible]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { CellProps, Column, TableInstance } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useSref } from '@uirouter/react';
|
import { useSref } from '@uirouter/react';
|
||||||
|
|
||||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
import type {
|
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||||
ContainersTableSettings,
|
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||||
DockerContainer,
|
|
||||||
} from '@/react/docker/containers/types';
|
|
||||||
|
|
||||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||||
|
|
||||||
|
import { TableSettings } from '../types';
|
||||||
|
|
||||||
export const name: Column<DockerContainer> = {
|
export const name: Column<DockerContainer> = {
|
||||||
Header: 'Name',
|
Header: 'Name',
|
||||||
|
@ -27,23 +27,24 @@ export const name: Column<DockerContainer> = {
|
||||||
export function NameCell({
|
export function NameCell({
|
||||||
value: name,
|
value: name,
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<TableInstance>) {
|
}: CellProps<DockerContainer>) {
|
||||||
const { settings } = useTableSettings<ContainersTableSettings>();
|
const linkProps = useSref('.container', {
|
||||||
const truncate = settings.truncateContainerName;
|
|
||||||
const endpoint = useEnvironment();
|
|
||||||
const offlineMode = endpoint.Status !== 1;
|
|
||||||
|
|
||||||
const linkProps = useSref('docker.containers.container', {
|
|
||||||
id: container.Id,
|
id: container.Id,
|
||||||
nodeName: container.NodeName,
|
nodeName: container.NodeName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { settings } = useTableSettings<TableSettings>();
|
||||||
|
const truncate = settings.truncateContainerName;
|
||||||
|
const environmentQuery = useCurrentEnvironment();
|
||||||
|
|
||||||
|
const environment = environmentQuery.data;
|
||||||
|
|
||||||
let shortName = name;
|
let shortName = name;
|
||||||
if (truncate > 0) {
|
if (truncate > 0) {
|
||||||
shortName = _.truncate(name, { length: truncate });
|
shortName = _.truncate(name, { length: truncate });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offlineMode) {
|
if (!environment || isOfflineEndpoint(environment)) {
|
||||||
return <span>{shortName}</span>;
|
return <span>{shortName}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Column } from 'react-table';
|
import { Column } from 'react-table';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
|
||||||
import type { DockerContainer, Port } from '@/react/docker/containers/types';
|
import type { DockerContainer, Port } from '@/react/docker/containers/types';
|
||||||
|
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||||
|
|
||||||
export const ports: Column<DockerContainer> = {
|
export const ports: Column<DockerContainer> = {
|
||||||
Header: 'Published Ports',
|
Header: 'Published Ports',
|
||||||
|
@ -20,12 +20,15 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PortsCell({ value: ports }: Props) {
|
function PortsCell({ value: ports }: Props) {
|
||||||
const { PublicURL: publicUrl } = useEnvironment();
|
const environmentQuery = useCurrentEnvironment();
|
||||||
|
|
||||||
if (ports.length === 0) {
|
const environment = environmentQuery.data;
|
||||||
|
if (!environment || ports.length === 0) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { PublicURL: publicUrl } = environment;
|
||||||
|
|
||||||
return _.uniqBy(ports, 'public').map((port) => (
|
return _.uniqBy(ports, 'public').map((port) => (
|
||||||
<a
|
<a
|
||||||
key={`${port.host}:${port.public}`}
|
key={`${port.host}:${port.public}`}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
|
|
||||||
import { useEnvironment } from '@/portainer/environments/useEnvironment';
|
|
||||||
import { useAuthorizations } from '@/portainer/hooks/useUser';
|
import { useAuthorizations } from '@/portainer/hooks/useUser';
|
||||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions/ContainerQuickActions';
|
import { isOfflineEndpoint } from '@/portainer/helpers/endpointHelper';
|
||||||
import type {
|
import { useCurrentEnvironment } from '@/portainer/hooks/useCurrentEnvironment';
|
||||||
ContainersTableSettings,
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
DockerContainer,
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
} from '@/react/docker/containers/types';
|
|
||||||
import { EnvironmentStatus } from '@/portainer/environments/types';
|
|
||||||
|
|
||||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||||
|
|
||||||
|
import { TableSettings } from '../types';
|
||||||
|
|
||||||
export const quickActions: Column<DockerContainer> = {
|
export const quickActions: Column<DockerContainer> = {
|
||||||
Header: 'Quick Actions',
|
Header: 'Quick Actions',
|
||||||
|
@ -25,10 +24,12 @@ export const quickActions: Column<DockerContainer> = {
|
||||||
function QuickActionsCell({
|
function QuickActionsCell({
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<DockerContainer>) {
|
}: CellProps<DockerContainer>) {
|
||||||
const endpoint = useEnvironment();
|
const environmentQuery = useCurrentEnvironment();
|
||||||
const offlineMode = endpoint.Status !== EnvironmentStatus.Up;
|
|
||||||
|
|
||||||
const { settings } = useTableSettings<ContainersTableSettings>();
|
const environment = environmentQuery.data;
|
||||||
|
const offlineMode = !environment || isOfflineEndpoint(environment);
|
||||||
|
|
||||||
|
const { settings } = useTableSettings<TableSettings>();
|
||||||
|
|
||||||
const { hiddenQuickActions = [] } = settings;
|
const { hiddenQuickActions = [] } = settings;
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { Column } from 'react-table';
|
import { CellProps, Column } from 'react-table';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
DockerContainer,
|
type DockerContainer,
|
||||||
DockerContainerStatus,
|
ContainerStatus,
|
||||||
} from '@/react/docker/containers/types';
|
} from '@/react/docker/containers/types';
|
||||||
|
|
||||||
import { DefaultFilter } from '@@/datatables/Filter';
|
import { DefaultFilter } from '@@/datatables/Filter';
|
||||||
|
@ -20,11 +19,14 @@ export const state: Column<DockerContainer> = {
|
||||||
canHide: true,
|
canHide: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function StatusCell({ value: status }: { value: DockerContainerStatus }) {
|
function StatusCell({
|
||||||
const statusNormalized = _.toLower(status);
|
value: status,
|
||||||
const hasHealthCheck = ['starting', 'healthy', 'unhealthy'].includes(
|
}: CellProps<DockerContainer, ContainerStatus>) {
|
||||||
statusNormalized
|
const hasHealthCheck = [
|
||||||
);
|
ContainerStatus.Starting,
|
||||||
|
ContainerStatus.Healthy,
|
||||||
|
ContainerStatus.Unhealthy,
|
||||||
|
].includes(status);
|
||||||
|
|
||||||
const statusClassName = getClassName();
|
const statusClassName = getClassName();
|
||||||
|
|
||||||
|
@ -40,22 +42,21 @@ function StatusCell({ value: status }: { value: DockerContainerStatus }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
function getClassName() {
|
function getClassName() {
|
||||||
if (includeString(['paused', 'starting', 'unhealthy'])) {
|
switch (status) {
|
||||||
|
case ContainerStatus.Paused:
|
||||||
|
case ContainerStatus.Starting:
|
||||||
|
case ContainerStatus.Unhealthy:
|
||||||
return 'warning';
|
return 'warning';
|
||||||
}
|
case ContainerStatus.Created:
|
||||||
|
|
||||||
if (includeString(['created'])) {
|
|
||||||
return 'info';
|
return 'info';
|
||||||
}
|
case ContainerStatus.Stopped:
|
||||||
|
case ContainerStatus.Dead:
|
||||||
if (includeString(['stopped', 'dead', 'exited'])) {
|
case ContainerStatus.Exited:
|
||||||
return 'danger';
|
return 'danger';
|
||||||
}
|
case ContainerStatus.Healthy:
|
||||||
|
case ContainerStatus.Running:
|
||||||
|
default:
|
||||||
return 'success';
|
return 'success';
|
||||||
|
|
||||||
function includeString(values: DockerContainerStatus[]) {
|
|
||||||
return values.some((val) => statusNormalized.includes(val));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import create from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
|
||||||
|
import {
|
||||||
|
paginationSettings,
|
||||||
|
sortableSettings,
|
||||||
|
refreshableSettings,
|
||||||
|
hiddenColumnsSettings,
|
||||||
|
} from '@/react/components/datatables/types';
|
||||||
|
|
||||||
|
import { QuickAction, TableSettings } from './types';
|
||||||
|
|
||||||
|
export const TRUNCATE_LENGTH = 32;
|
||||||
|
|
||||||
|
export function createStore(storageKey: string) {
|
||||||
|
return create<TableSettings>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...sortableSettings(set),
|
||||||
|
...paginationSettings(set),
|
||||||
|
...hiddenColumnsSettings(set),
|
||||||
|
...refreshableSettings(set),
|
||||||
|
truncateContainerName: TRUNCATE_LENGTH,
|
||||||
|
setTruncateContainerName(truncateContainerName: number) {
|
||||||
|
set({
|
||||||
|
truncateContainerName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hiddenQuickActions: [] as QuickAction[],
|
||||||
|
setHiddenQuickActions: (hiddenQuickActions: QuickAction[]) =>
|
||||||
|
set({ hiddenQuickActions }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: keyBuilder(storageKey),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { ContainersDatatable } from './ContainersDatatable';
|
|
@ -0,0 +1,23 @@
|
||||||
|
import {
|
||||||
|
PaginationTableSettings,
|
||||||
|
RefreshableTableSettings,
|
||||||
|
SettableColumnsTableSettings,
|
||||||
|
SortableTableSettings,
|
||||||
|
} from '@/react/components/datatables/types';
|
||||||
|
|
||||||
|
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
||||||
|
|
||||||
|
export interface SettableQuickActionsTableSettings<TAction> {
|
||||||
|
hiddenQuickActions: TAction[];
|
||||||
|
setHiddenQuickActions: (hiddenQuickActions: TAction[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableSettings
|
||||||
|
extends SortableTableSettings,
|
||||||
|
PaginationTableSettings,
|
||||||
|
SettableColumnsTableSettings,
|
||||||
|
SettableQuickActionsTableSettings<QuickAction>,
|
||||||
|
RefreshableTableSettings {
|
||||||
|
truncateContainerName: number;
|
||||||
|
setTruncateContainerName: (value: number) => void;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { useInfo } from '@/docker/services/system.service';
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
import { isAgentEnvironment } from '@/portainer/environments/utils';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
import { ContainersDatatable } from './ContainersDatatable';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
endpoint: Environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListView({ endpoint: environment }: Props) {
|
||||||
|
const isAgent = isAgentEnvironment(environment.Type);
|
||||||
|
|
||||||
|
const envInfoQuery = useInfo(environment.Id, (info) => !!info.Swarm?.NodeID);
|
||||||
|
|
||||||
|
const isSwarmManager = !!envInfoQuery.data;
|
||||||
|
const isHostColumnVisible = isAgent && isSwarmManager;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Container list"
|
||||||
|
breadcrumbs={[{ label: 'Containers' }]}
|
||||||
|
reload
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<ContainersDatatable
|
||||||
|
isHostColumnVisible={isHostColumnVisible}
|
||||||
|
environment={environment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { ListView } from './ListView';
|
|
@ -1,10 +1,9 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { DockerContainerStatus } from '@/react/docker/containers/types';
|
import { ContainerStatus } from '@/react/docker/containers/types';
|
||||||
import { Authorized } from '@/portainer/hooks/useUser';
|
import { Authorized } from '@/portainer/hooks/useUser';
|
||||||
import { Icon } from '@/react/components/Icon';
|
|
||||||
import { react2angular } from '@/react-tools/react2angular';
|
|
||||||
|
|
||||||
|
import { Icon } from '@@/Icon';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
import styles from './ContainerQuickActions.module.css';
|
import styles from './ContainerQuickActions.module.css';
|
||||||
|
@ -22,7 +21,7 @@ interface Props {
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
state: QuickActionsState;
|
state: QuickActionsState;
|
||||||
status: DockerContainerStatus;
|
status: ContainerStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContainerQuickActions({
|
export function ContainerQuickActions({
|
||||||
|
@ -36,9 +35,12 @@ export function ContainerQuickActions({
|
||||||
return <TaskQuickActions taskId={taskId} state={state} />;
|
return <TaskQuickActions taskId={taskId} state={state} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActive = ['starting', 'running', 'healthy', 'unhealthy'].includes(
|
const isActive = [
|
||||||
status
|
ContainerStatus.Starting,
|
||||||
);
|
ContainerStatus.Running,
|
||||||
|
ContainerStatus.Healthy,
|
||||||
|
ContainerStatus.Unhealthy,
|
||||||
|
].includes(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('space-x-1', styles.root)}>
|
<div className={clsx('space-x-1', styles.root)}>
|
||||||
|
@ -135,8 +137,3 @@ function TaskQuickActions({ taskId, state }: TaskProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContainerQuickActionsAngular = react2angular(
|
|
||||||
ContainerQuickActions,
|
|
||||||
['taskId', 'containerId', 'nodeName', 'state', 'status']
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
export {
|
export { ContainerQuickActions } from './ContainerQuickActions';
|
||||||
ContainerQuickActions,
|
|
||||||
ContainerQuickActionsAngular,
|
|
||||||
} from './ContainerQuickActions';
|
|
||||||
|
|
|
@ -3,15 +3,8 @@ import PortainerError from '@/portainer/error';
|
||||||
import axios from '@/portainer/services/axios';
|
import axios from '@/portainer/services/axios';
|
||||||
import { genericHandler } from '@/docker/rest/response/handlers';
|
import { genericHandler } from '@/docker/rest/response/handlers';
|
||||||
|
|
||||||
import { NetworkId } from '../networks/types';
|
|
||||||
|
|
||||||
import { ContainerId, DockerContainer } from './types';
|
import { ContainerId, DockerContainer } from './types';
|
||||||
|
|
||||||
export interface Filters {
|
|
||||||
label?: string[];
|
|
||||||
network?: NetworkId[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startContainer(
|
export async function startContainer(
|
||||||
endpointId: EnvironmentId,
|
endpointId: EnvironmentId,
|
||||||
id: ContainerId
|
id: ContainerId
|
||||||
|
@ -92,25 +85,7 @@ export async function removeContainer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContainers(
|
export function urlBuilder(
|
||||||
environmentId: EnvironmentId,
|
|
||||||
filters?: Filters
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get<DockerContainer[]>(
|
|
||||||
urlBuilder(environmentId, '', 'json'),
|
|
||||||
{
|
|
||||||
params: { all: 0, filters },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
throw new PortainerError('Unable to retrieve containers', e as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlBuilder(
|
|
||||||
endpointId: EnvironmentId,
|
endpointId: EnvironmentId,
|
||||||
id?: ContainerId,
|
id?: ContainerId,
|
||||||
action?: string
|
action?: string
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { useQuery } from 'react-query';
|
|
||||||
|
|
||||||
import { EnvironmentId } from '@/portainer/environments/types';
|
|
||||||
|
|
||||||
import { getContainers, Filters } from './containers.service';
|
|
||||||
|
|
||||||
export function useContainers(environmentId: EnvironmentId, filters?: Filters) {
|
|
||||||
return useQuery(
|
|
||||||
['environments', environmentId, 'docker', 'containers', { filters }],
|
|
||||||
() => getContainers(environmentId, filters),
|
|
||||||
{
|
|
||||||
meta: {
|
|
||||||
title: 'Failure',
|
|
||||||
message: 'Unable to get containers in network',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { urlBuilder } from '../containers.service';
|
||||||
|
import { DockerContainerResponse } from '../types/response';
|
||||||
|
import { parseViewModel } from '../utils';
|
||||||
|
|
||||||
|
import { Filters } from './types';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useContainers(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
all = true,
|
||||||
|
filters?: Filters,
|
||||||
|
autoRefreshRate?: number
|
||||||
|
) {
|
||||||
|
return useQuery(
|
||||||
|
queryKeys.filters(environmentId, all, filters),
|
||||||
|
() => getContainers(environmentId, all, filters),
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
title: 'Failure',
|
||||||
|
message: 'Unable to retrieve containers',
|
||||||
|
},
|
||||||
|
refetchInterval() {
|
||||||
|
return autoRefreshRate ?? false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getContainers(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
all = true,
|
||||||
|
filters?: Filters
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<DockerContainerResponse[]>(
|
||||||
|
urlBuilder(environmentId, undefined, 'json'),
|
||||||
|
{
|
||||||
|
params: { all, filters: filters && JSON.stringify(filters) },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.map((c) => parseViewModel(c));
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error as Error, 'Unable to retrieve containers');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
async function getContainerGpus(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
containerId: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<{ gpus: string }>(
|
||||||
|
`/docker/${environmentId}/containers/${containerId}/gpus`
|
||||||
|
);
|
||||||
|
return data.gpus;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContainerGpus(
|
||||||
|
environmentId: EnvironmentId,
|
||||||
|
containerId: string
|
||||||
|
) {
|
||||||
|
return useQuery(queryKeys.gpus(environmentId, containerId), () =>
|
||||||
|
getContainerGpus(environmentId, containerId)
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
import { queryKeys as dockerQueryKeys } from '../../queries/utils';
|
||||||
|
|
||||||
|
import { Filters } from './types';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
list: (environmentId: EnvironmentId) =>
|
||||||
|
[dockerQueryKeys.root(environmentId), 'containers'] as const,
|
||||||
|
|
||||||
|
filters: (environmentId: EnvironmentId, all?: boolean, filters?: Filters) =>
|
||||||
|
[...queryKeys.list(environmentId), { all, filters }] as const,
|
||||||
|
|
||||||
|
container: (environmentId: EnvironmentId, id: string) =>
|
||||||
|
[...queryKeys.list(environmentId), id] as const,
|
||||||
|
|
||||||
|
gpus: (environmentId: EnvironmentId, id: string) =>
|
||||||
|
[...queryKeys.container(environmentId, id), 'gpus'] as const,
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { NetworkId } from '../../networks/types';
|
||||||
|
|
||||||
|
export interface Filters {
|
||||||
|
label?: string[];
|
||||||
|
network?: NetworkId[];
|
||||||
|
}
|
|
@ -1,54 +1,40 @@
|
||||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
import {
|
import { DockerContainerResponse } from './types/response';
|
||||||
PaginationTableSettings,
|
|
||||||
RefreshableTableSettings,
|
|
||||||
SettableColumnsTableSettings,
|
|
||||||
SettableQuickActionsTableSettings,
|
|
||||||
SortableTableSettings,
|
|
||||||
} from '@@/datatables/types';
|
|
||||||
|
|
||||||
export type DockerContainerStatus =
|
export enum ContainerStatus {
|
||||||
| 'paused'
|
Paused = 'paused',
|
||||||
| 'stopped'
|
Stopped = 'stopped',
|
||||||
| 'created'
|
Created = 'created',
|
||||||
| 'healthy'
|
Healthy = 'healthy',
|
||||||
| 'unhealthy'
|
Unhealthy = 'unhealthy',
|
||||||
| 'starting'
|
Starting = 'starting',
|
||||||
| 'running'
|
Running = 'running',
|
||||||
| 'dead'
|
Dead = 'dead',
|
||||||
| 'exited';
|
Exited = 'exited',
|
||||||
|
}
|
||||||
|
|
||||||
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
||||||
|
|
||||||
export interface ContainersTableSettings
|
|
||||||
extends SortableTableSettings,
|
|
||||||
PaginationTableSettings,
|
|
||||||
SettableColumnsTableSettings,
|
|
||||||
SettableQuickActionsTableSettings<QuickAction>,
|
|
||||||
RefreshableTableSettings {
|
|
||||||
truncateContainerName: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Port {
|
export interface Port {
|
||||||
host: string;
|
host?: string;
|
||||||
public: string;
|
public: number;
|
||||||
private: string;
|
private: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContainerId = string;
|
export type ContainerId = string;
|
||||||
|
|
||||||
export type DockerContainer = {
|
type DecoratedDockerContainer = {
|
||||||
IsPortainer: boolean;
|
|
||||||
Status: DockerContainerStatus;
|
|
||||||
NodeName: string;
|
NodeName: string;
|
||||||
Id: ContainerId;
|
ResourceControl?: ResourceControlViewModel;
|
||||||
IP: string;
|
IP: string;
|
||||||
Names: string[];
|
|
||||||
Created: string;
|
|
||||||
ResourceControl: ResourceControlViewModel;
|
|
||||||
Ports: Port[];
|
|
||||||
StackName?: string;
|
StackName?: string;
|
||||||
|
Status: ContainerStatus;
|
||||||
|
Ports: Port[];
|
||||||
|
StatusText: string;
|
||||||
Image: string;
|
Image: string;
|
||||||
Gpus: string;
|
Gpus: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DockerContainer = DecoratedDockerContainer &
|
||||||
|
Omit<DockerContainerResponse, keyof DecoratedDockerContainer>;
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { PortainerMetadata } from '@/react/docker/types';
|
||||||
|
|
||||||
|
interface EndpointIPAMConfig {
|
||||||
|
IPv4Address?: string;
|
||||||
|
IPv6Address?: string;
|
||||||
|
LinkLocalIPs?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EndpointSettings {
|
||||||
|
IPAMConfig?: EndpointIPAMConfig;
|
||||||
|
Links: string[];
|
||||||
|
Aliases: string[];
|
||||||
|
NetworkID: string;
|
||||||
|
EndpointID: string;
|
||||||
|
Gateway: string;
|
||||||
|
IPAddress: string;
|
||||||
|
IPPrefixLen: number;
|
||||||
|
IPv6Gateway: string;
|
||||||
|
GlobalIPv6Address: string;
|
||||||
|
GlobalIPv6PrefixLen: number;
|
||||||
|
MacAddress: string;
|
||||||
|
DriverOpts: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryNetworkSettings {
|
||||||
|
Networks: { [key: string]: EndpointSettings | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortResponse {
|
||||||
|
IP?: string;
|
||||||
|
PrivatePort: number;
|
||||||
|
PublicPort?: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MountPropagation {
|
||||||
|
// PropagationRPrivate RPRIVATE
|
||||||
|
RPrivate = 'rprivate',
|
||||||
|
// PropagationPrivate PRIVATE
|
||||||
|
Private = 'private',
|
||||||
|
// PropagationRShared RSHARED
|
||||||
|
RShared = 'rshared',
|
||||||
|
// PropagationShared SHARED
|
||||||
|
Shared = 'shared',
|
||||||
|
// PropagationRSlave RSLAVE
|
||||||
|
RSlave = 'rslave',
|
||||||
|
// PropagationSlave SLAVE
|
||||||
|
Slave = 'slave',
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MountType {
|
||||||
|
// TypeBind is the type for mounting host dir
|
||||||
|
Bind = 'bind',
|
||||||
|
// TypeVolume is the type for remote storage volumes
|
||||||
|
Volume = 'volume',
|
||||||
|
// TypeTmpfs is the type for mounting tmpfs
|
||||||
|
Tmpfs = 'tmpfs',
|
||||||
|
// TypeNamedPipe is the type for mounting Windows named pipes
|
||||||
|
NamedPipe = 'npipe',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MountPoint {
|
||||||
|
Type?: MountType;
|
||||||
|
Name?: string;
|
||||||
|
Source: string;
|
||||||
|
Destination: string;
|
||||||
|
Driver?: string;
|
||||||
|
Mode: string;
|
||||||
|
RW: boolean;
|
||||||
|
Propagation: MountPropagation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockerContainerResponse {
|
||||||
|
Id: string;
|
||||||
|
Names: string[];
|
||||||
|
Image: string;
|
||||||
|
ImageID: string;
|
||||||
|
Command: string;
|
||||||
|
Created: number;
|
||||||
|
Ports: PortResponse[];
|
||||||
|
SizeRw?: number;
|
||||||
|
SizeRootFs?: number;
|
||||||
|
Labels: { [key: string]: string };
|
||||||
|
State: string;
|
||||||
|
Status: string;
|
||||||
|
HostConfig: {
|
||||||
|
NetworkMode?: string;
|
||||||
|
};
|
||||||
|
NetworkSettings?: SummaryNetworkSettings;
|
||||||
|
Mounts: MountPoint[];
|
||||||
|
|
||||||
|
Portainer: PortainerMetadata;
|
||||||
|
IsPortainer: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
|
import { DockerContainer, ContainerStatus } from './types';
|
||||||
|
import { DockerContainerResponse } from './types/response';
|
||||||
|
|
||||||
|
export function parseViewModel(
|
||||||
|
response: DockerContainerResponse
|
||||||
|
): DockerContainer {
|
||||||
|
const resourceControl =
|
||||||
|
response.Portainer?.ResourceControl &&
|
||||||
|
new ResourceControlViewModel(response?.Portainer?.ResourceControl);
|
||||||
|
const nodeName = response.Portainer?.Agent?.NodeName || '';
|
||||||
|
|
||||||
|
const ip =
|
||||||
|
Object.values(response?.NetworkSettings?.Networks || {})[0]?.IPAddress ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const labels = response.Labels || {};
|
||||||
|
const stackName =
|
||||||
|
labels['com.docker.compose.project'] ||
|
||||||
|
labels['com.docker.stack.namespace'];
|
||||||
|
|
||||||
|
const status = createStatus(response.Status);
|
||||||
|
|
||||||
|
const ports = _.compact(
|
||||||
|
response.Ports?.map(
|
||||||
|
(p) =>
|
||||||
|
p.PublicPort && {
|
||||||
|
host: p.IP,
|
||||||
|
private: p.PrivatePort,
|
||||||
|
public: p.PublicPort,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
ResourceControl: resourceControl,
|
||||||
|
NodeName: nodeName,
|
||||||
|
IP: ip,
|
||||||
|
StackName: stackName,
|
||||||
|
Status: status,
|
||||||
|
Ports: ports,
|
||||||
|
StatusText: response.Status,
|
||||||
|
Gpus: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStatus(statusText = ''): ContainerStatus {
|
||||||
|
const status = statusText.toLowerCase();
|
||||||
|
|
||||||
|
if (status.includes(ContainerStatus.Paused)) {
|
||||||
|
return ContainerStatus.Paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes(ContainerStatus.Dead)) {
|
||||||
|
return ContainerStatus.Dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes(ContainerStatus.Created)) {
|
||||||
|
return ContainerStatus.Created;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes(ContainerStatus.Stopped)) {
|
||||||
|
return ContainerStatus.Stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes(ContainerStatus.Exited)) {
|
||||||
|
return ContainerStatus.Exited;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes('(healthy)')) {
|
||||||
|
return ContainerStatus.Healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes('(unhealthy)')) {
|
||||||
|
return ContainerStatus.Unhealthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.includes('(health: starting)')) {
|
||||||
|
return ContainerStatus.Starting;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ContainerStatus.Running;
|
||||||
|
}
|
|
@ -9,13 +9,13 @@ import { AccessControlPanel } from '@/portainer/access-control/AccessControlPane
|
||||||
import { ResourceControlType } from '@/portainer/access-control/types';
|
import { ResourceControlType } from '@/portainer/access-control/types';
|
||||||
import { DockerContainer } from '@/react/docker/containers/types';
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
import { useContainers } from '@/react/docker/containers/queries/containers';
|
||||||
|
|
||||||
import { PageHeader } from '@@/PageHeader';
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||||
import { isSystemNetwork } from '../network.helper';
|
import { isSystemNetwork } from '../network.helper';
|
||||||
import { DockerNetwork, NetworkContainer } from '../types';
|
import { DockerNetwork, NetworkContainer } from '../types';
|
||||||
import { useContainers } from '../../containers/queries';
|
|
||||||
|
|
||||||
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
import { NetworkDetailsTable } from './NetworkDetailsTable';
|
||||||
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
import { NetworkOptionsTable } from './NetworkOptionsTable';
|
||||||
|
@ -38,7 +38,7 @@ export function ItemView() {
|
||||||
const filters = {
|
const filters = {
|
||||||
network: [networkId],
|
network: [networkId],
|
||||||
};
|
};
|
||||||
const containersQuery = useContainers(environmentId, filters);
|
const containersQuery = useContainers(environmentId, true, filters);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (networkQuery.data && containersQuery.data) {
|
if (networkQuery.data && containersQuery.data) {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { EnvironmentId } from '@/portainer/environments/types';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
root: (environmentId: EnvironmentId) => ['docker', environmentId] as const,
|
||||||
|
};
|
|
@ -0,0 +1,99 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
import { createStore } from '@/react/docker/containers/ListView/ContainersDatatable/datatable-store';
|
||||||
|
import { useColumns } from '@/react/docker/containers/ListView/ContainersDatatable/columns';
|
||||||
|
import { ContainersDatatableActions } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableActions';
|
||||||
|
import { ContainersDatatableSettings } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableSettings';
|
||||||
|
|
||||||
|
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||||
|
import {
|
||||||
|
buildAction,
|
||||||
|
QuickActionsSettings,
|
||||||
|
} from '@@/datatables/QuickActionsSettings';
|
||||||
|
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||||
|
|
||||||
|
import { useContainers } from '../../containers/queries/containers';
|
||||||
|
import { RowProvider } from '../../containers/ListView/ContainersDatatable/RowContext';
|
||||||
|
|
||||||
|
const storageKey = 'stack-containers';
|
||||||
|
const useStore = createStore(storageKey);
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
buildAction('logs', 'Logs'),
|
||||||
|
buildAction('inspect', 'Inspect'),
|
||||||
|
buildAction('stats', 'Stats'),
|
||||||
|
buildAction('exec', 'Console'),
|
||||||
|
buildAction('attach', 'Attach'),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
environment: Environment;
|
||||||
|
stackName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StackContainersDatatable({ environment, stackName }: Props) {
|
||||||
|
const settings = useStore();
|
||||||
|
const columns = useColumns(false);
|
||||||
|
const hidableColumns = _.compact(
|
||||||
|
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const containersQuery = useContainers(
|
||||||
|
environment.Id,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
label: [`com.docker.compose.project=${stackName}`],
|
||||||
|
},
|
||||||
|
settings.autoRefreshRate * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RowProvider context={{ environment }}>
|
||||||
|
<Datatable
|
||||||
|
titleOptions={{
|
||||||
|
icon: 'fa-cubes',
|
||||||
|
title: 'Containers',
|
||||||
|
}}
|
||||||
|
settingsStore={settings}
|
||||||
|
columns={columns}
|
||||||
|
renderTableActions={(selectedRows) => (
|
||||||
|
<ContainersDatatableActions
|
||||||
|
selectedItems={selectedRows}
|
||||||
|
isAddActionVisible={false}
|
||||||
|
endpointId={environment.Id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||||
|
renderTableSettings={(tableInstance) => {
|
||||||
|
const columnsToHide = tableInstance.allColumns.filter((colInstance) =>
|
||||||
|
hidableColumns?.includes(colInstance.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ColumnVisibilityMenu<DockerContainer>
|
||||||
|
columns={columnsToHide}
|
||||||
|
onChange={(hiddenColumns) => {
|
||||||
|
settings.setHiddenColumns(hiddenColumns);
|
||||||
|
tableInstance.setHiddenColumns(hiddenColumns);
|
||||||
|
}}
|
||||||
|
value={settings.hiddenColumns}
|
||||||
|
/>
|
||||||
|
<TableSettingsMenu
|
||||||
|
quickActions={<QuickActionsSettings actions={actions} />}
|
||||||
|
>
|
||||||
|
<ContainersDatatableSettings settings={settings} />
|
||||||
|
</TableSettingsMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
storageKey={storageKey}
|
||||||
|
dataset={containersQuery.data || []}
|
||||||
|
isLoading={containersQuery.isLoading}
|
||||||
|
emptyContentLabel="No containers found"
|
||||||
|
/>
|
||||||
|
</RowProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
export enum StackType {
|
||||||
|
/**
|
||||||
|
* Represents a stack managed via docker stack
|
||||||
|
*/
|
||||||
|
DockerSwarm = 1,
|
||||||
|
/**
|
||||||
|
* Represents a stack managed via docker-compose
|
||||||
|
*/
|
||||||
|
DockerCompose,
|
||||||
|
/**
|
||||||
|
* Represents a stack managed via kubectl
|
||||||
|
*/
|
||||||
|
Kubernetes,
|
||||||
|
/**
|
||||||
|
* Represents a stack managed via Nomad
|
||||||
|
*/
|
||||||
|
Nomad,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum StackStatus {
|
||||||
|
Active = 1,
|
||||||
|
Inactive,
|
||||||
|
}
|
|
@ -1,5 +1,10 @@
|
||||||
import { ResourceControlResponse } from '@/portainer/access-control/types';
|
import { ResourceControlResponse } from '@/portainer/access-control/types';
|
||||||
|
|
||||||
export interface PortainerMetadata {
|
interface AgentMetadata {
|
||||||
ResourceControl: ResourceControlResponse;
|
NodeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PortainerMetadata {
|
||||||
|
ResourceControl?: ResourceControlResponse;
|
||||||
|
Agent?: AgentMetadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,8 @@
|
||||||
"x256": "^0.0.2",
|
"x256": "^0.0.2",
|
||||||
"xterm": "^3.8.0",
|
"xterm": "^3.8.0",
|
||||||
"yaml": "^1.10.2",
|
"yaml": "^1.10.2",
|
||||||
"yup": "^0.32.11"
|
"yup": "^0.32.11",
|
||||||
|
"zustand": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@apidevtools/swagger-cli": "^4.0.4",
|
"@apidevtools/swagger-cli": "^4.0.4",
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -18179,6 +18179,11 @@ use-sidecar@^1.1.2:
|
||||||
detect-node-es "^1.1.0"
|
detect-node-es "^1.1.0"
|
||||||
tslib "^2.0.0"
|
tslib "^2.0.0"
|
||||||
|
|
||||||
|
use-sync-external-store@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
use@^3.1.0:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
|
@ -19038,6 +19043,13 @@ z-schema@^5.0.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
commander "^2.7.1"
|
commander "^2.7.1"
|
||||||
|
|
||||||
|
zustand@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.0.0.tgz#739cba69209ffe67b31e7d6741c25b51496114a7"
|
||||||
|
integrity sha512-OrsfQTnRXF1LZ9/vR/IqN9ws5EXUhb149xmPjErZnUrkgxS/gAHGy2dPNIVkVvoxrVe1sIydn4JjF0dYHmGeeQ==
|
||||||
|
dependencies:
|
||||||
|
use-sync-external-store "1.2.0"
|
||||||
|
|
||||||
zwitch@^1.0.0:
|
zwitch@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
|
||||||
|
|
Loading…
Reference in New Issue