mirror of https://github.com/portainer/portainer
feat(docker/stacks): add creation and update dates (#4418)
* feat(docker/stacks): add creation and update dates * feat(docker/stacks): put ownership column as the last column * feat(docker/stacks): fix the no stacks messagepull/4516/head
parent
4bc958f865
commit
bd98b8956a
|
@ -7,6 +7,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -60,13 +61,14 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
|
@ -146,13 +148,14 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: payload.ComposeFilePathInRepository,
|
EntryPoint: payload.ComposeFilePathInRepository,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||||
|
@ -242,13 +245,14 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerComposeStack,
|
Type: portainer.DockerComposeStack,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -55,14 +56,15 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
|
@ -145,14 +147,15 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: payload.ComposeFilePathInRepository,
|
EntryPoint: payload.ComposeFilePathInRepository,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||||
|
@ -249,14 +252,15 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||||
|
|
||||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||||
stack := &portainer.Stack{
|
stack := &portainer.Stack{
|
||||||
ID: portainer.StackID(stackID),
|
ID: portainer.StackID(stackID),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Type: portainer.DockerSwarmStack,
|
Type: portainer.DockerSwarmStack,
|
||||||
SwarmID: payload.SwarmID,
|
SwarmID: payload.SwarmID,
|
||||||
EndpointID: endpoint.ID,
|
EndpointID: endpoint.ID,
|
||||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||||
Env: payload.Env,
|
Env: payload.Env,
|
||||||
Status: portainer.StackStatusActive,
|
Status: portainer.StackStatusActive,
|
||||||
|
CreationDate: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -123,6 +124,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.Env = payload.Env
|
stack.Env = payload.Env
|
||||||
|
stack.UpdateDate = time.Now().Unix()
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||||
|
@ -151,6 +153,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||||
}
|
}
|
||||||
|
|
||||||
stack.Env = payload.Env
|
stack.Env = payload.Env
|
||||||
|
stack.UpdateDate = time.Now().Unix()
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
stackFolder := strconv.Itoa(int(stack.ID))
|
||||||
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||||
|
|
|
@ -554,6 +554,8 @@ type (
|
||||||
Env []Pair `json:"Env"`
|
Env []Pair `json:"Env"`
|
||||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||||
Status StackStatus `json:"Status"`
|
Status StackStatus `json:"Status"`
|
||||||
|
CreationDate int64 `json:"CreationDate"`
|
||||||
|
UpdateDate int64 `json:"UpdateDate"`
|
||||||
ProjectPath string
|
ProjectPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,20 @@
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>Control</th>
|
<th>Control</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('ResourceControl.CreationDate')">
|
||||||
|
Created
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.CreationDate' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('ResourceControl.UpdateDate')">
|
||||||
|
Updated
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ResourceControl.UpdateDate' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
<a ng-click="$ctrl.changeOrderBy('ResourceControl.Ownership')">
|
||||||
Ownership
|
Ownership
|
||||||
|
@ -154,6 +168,14 @@
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="!item.External">Total</span>
|
<span ng-if="!item.External">Total</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<span ng-if="item.CreationDate">{{ item.CreationDate | getisodatefromtimestamp }}</span>
|
||||||
|
<span ng-if="!item.CreationDate"> - </span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span ng-if="item.UpdateDate">{{ item.UpdateDate | getisodatefromtimestamp }}</span>
|
||||||
|
<span ng-if="!item.UpdateDate"> - </span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
|
@ -165,7 +187,7 @@
|
||||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||||
<td colspan="4" class="text-center text-muted">No stack available.</td>
|
<td colspan="6" class="text-center text-muted">No stack available.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -5,31 +5,31 @@ angular.module('portainer.app').factory('StackHelper', [
|
||||||
'use strict';
|
'use strict';
|
||||||
var helper = {};
|
var helper = {};
|
||||||
|
|
||||||
helper.getExternalStackNamesFromContainers = function (containers) {
|
helper.getExternalStacksFromContainers = function (containers) {
|
||||||
var stackNames = [];
|
var stacks = [];
|
||||||
|
|
||||||
for (var i = 0; i < containers.length; i++) {
|
for (var i = 0; i < containers.length; i++) {
|
||||||
var container = containers[i];
|
var container = containers[i];
|
||||||
if (!container.Labels || !container.Labels['com.docker.compose.project']) continue;
|
if (!container.Labels || !container.Labels['com.docker.compose.project']) continue;
|
||||||
var stackName = container.Labels['com.docker.compose.project'];
|
var stackName = container.Labels['com.docker.compose.project'];
|
||||||
stackNames.push(stackName);
|
stacks.push({ stackName, creationDate: container.Created });
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniq(stackNames);
|
return _.uniq(stacks);
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.getExternalStackNamesFromServices = function (services) {
|
helper.getExternalStacksFromServices = function (services) {
|
||||||
var stackNames = [];
|
var stacks = [];
|
||||||
|
|
||||||
for (var i = 0; i < services.length; i++) {
|
for (var i = 0; i < services.length; i++) {
|
||||||
var service = services[i];
|
var service = services[i];
|
||||||
if (!service.Labels || !service.Labels['com.docker.stack.namespace']) continue;
|
if (!service.Labels || !service.Labels['com.docker.stack.namespace']) continue;
|
||||||
|
|
||||||
var stackName = service.Labels['com.docker.stack.namespace'];
|
var stackName = service.Labels['com.docker.stack.namespace'];
|
||||||
stackNames.push(stackName);
|
stacks.push({ stackName, creationDate: service.Created });
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniq(stackNames);
|
return _.uniq(stacks);
|
||||||
};
|
};
|
||||||
|
|
||||||
return helper;
|
return helper;
|
||||||
|
|
|
@ -13,11 +13,14 @@ export function StackViewModel(data) {
|
||||||
}
|
}
|
||||||
this.External = false;
|
this.External = false;
|
||||||
this.Status = data.Status;
|
this.Status = data.Status;
|
||||||
|
this.CreationDate = data.CreationDate;
|
||||||
|
this.UpdateDate = data.UpdateDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExternalStackViewModel(name, type) {
|
export function ExternalStackViewModel(name, type, creationDate) {
|
||||||
this.Name = name;
|
this.Name = name;
|
||||||
this.Type = type;
|
this.Type = type;
|
||||||
this.External = true;
|
this.External = true;
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
|
this.CreationDate = creationDate;
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,9 +123,9 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
ServiceService.services()
|
ServiceService.services()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var services = data;
|
var services = data;
|
||||||
var stackNames = StackHelper.getExternalStackNamesFromServices(services);
|
var stackDatas = StackHelper.getExternalStacksFromServices(services);
|
||||||
var stacks = stackNames.map(function (name) {
|
var stacks = stackDatas.map(function (stack) {
|
||||||
return new ExternalStackViewModel(name, 1);
|
return new ExternalStackViewModel(stack.stackName, 1, stack.creationDate);
|
||||||
});
|
});
|
||||||
deferred.resolve(stacks);
|
deferred.resolve(stacks);
|
||||||
})
|
})
|
||||||
|
@ -142,9 +142,9 @@ angular.module('portainer.app').factory('StackService', [
|
||||||
ContainerService.containers(1)
|
ContainerService.containers(1)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var containers = data;
|
var containers = data;
|
||||||
var stackNames = StackHelper.getExternalStackNamesFromContainers(containers);
|
var stacksDatas = StackHelper.getExternalStacksFromContainers(containers);
|
||||||
var stacks = stackNames.map(function (name) {
|
var stacks = stacksDatas.map(function (stack) {
|
||||||
return new ExternalStackViewModel(name, 2);
|
return new ExternalStackViewModel(stack.stackName, 2, stack.creationDate);
|
||||||
});
|
});
|
||||||
deferred.resolve(stacks);
|
deferred.resolve(stacks);
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue