mirror of https://github.com/portainer/portainer
feat(endpoints): add the ability to browse offline endpoints (#2253)
* feat(back): saved data in snapshot * feat(endpoints): adding interceptors to retrieve saved data on offline endpoints * feat(endpoints): offline dashboard working - need tests on offline views * refactor(endpoints): interceptors cleaning and saving/loading offline endpoints data in/from localstorage * feat(endpoints): browsing offline endpoints * feat(endpoints): removing all the link in offline mode - sidebar not working when switching between off and on modes w/ stateManager logic * feat(endpoints): endpoint status detection in real time * fix(endpoints): offline swarm endpoint are not accessible anymore * fix(endpoints): refactor message + disable offline browsing for an endpoint when no snapshot is available for it * fix(endpoints): adding timeout and enabling loading bar for offline requests * fix(endpoints): trying to access a down endpoint wont remove sidebar items if it fails * feat(endpoints): disable checkboxes on offline views for offline mode * feat(endpoints): updating endpoint status when detecting a change * refactor(host): moved offline status panel from engine view to new host view * fix(endpoints): missing endpoint update on ping from home view * fix(api): rework EndpointUpdate operation * refactor(offline): moved endpoint status to EndpointProvider and refactor the status-changed detection * fix(offline): moved status detection to callback on views -> prevent displaying the offline message when endpoint is back online on view change * fix(offline): offline message is now displayed online when browsing an offline endpoint * fix(offline): sidebar updates correctly on endpoint status change * fix(offline): offline panel not displayed and hidden on online mode * refactor(offline): rework of OfflineMode management * refactor(offline): extract information-panel for offlineMode into a component * refactor(offline): remove redundant binding of informationPanel + endpointStatusInterceptor patter as service * refactor(interceptors): moved interceptors pattern to service pattern * feat(stacks): prevent inspection of a stack in offline mode * feat(host): hide devices/disk panels in offline mode * feat(host): disable browse action in offline mode * refactor(home): remove commentspull/2419/head
parent
354fda31f1
commit
a61654a35d
|
@ -52,6 +52,16 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = snapshotNetworks(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotVersion(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
snapshot.Time = time.Now().Unix()
|
snapshot.Time = time.Now().Unix()
|
||||||
return snapshot, nil
|
return snapshot, nil
|
||||||
}
|
}
|
||||||
|
@ -66,6 +76,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
snapshot.DockerVersion = info.ServerVersion
|
snapshot.DockerVersion = info.ServerVersion
|
||||||
snapshot.TotalCPU = info.NCPU
|
snapshot.TotalCPU = info.NCPU
|
||||||
snapshot.TotalMemory = info.MemTotal
|
snapshot.TotalMemory = info.MemTotal
|
||||||
|
snapshot.SnapshotRaw.Info = info
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +143,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
|
||||||
snapshot.RunningContainerCount = runningContainers
|
snapshot.RunningContainerCount = runningContainers
|
||||||
snapshot.StoppedContainerCount = stoppedContainers
|
snapshot.StoppedContainerCount = stoppedContainers
|
||||||
snapshot.StackCount += len(stacks)
|
snapshot.StackCount += len(stacks)
|
||||||
|
snapshot.SnapshotRaw.Containers = containers
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,6 +154,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.ImageCount = len(images)
|
snapshot.ImageCount = len(images)
|
||||||
|
snapshot.SnapshotRaw.Images = images
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,5 +165,24 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot.VolumeCount = len(volumes.Volumes)
|
snapshot.VolumeCount = len(volumes.Volumes)
|
||||||
|
snapshot.SnapshotRaw.Volumes = volumes
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
snapshot.SnapshotRaw.Networks = networks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
version, err := cli.ServerVersion(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
snapshot.SnapshotRaw.Version = version
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type endpointUpdatePayload struct {
|
type endpointUpdatePayload struct {
|
||||||
Name string
|
Name *string
|
||||||
URL string
|
URL *string
|
||||||
PublicURL string
|
PublicURL *string
|
||||||
GroupID int
|
GroupID *int
|
||||||
TLS bool
|
TLS *bool
|
||||||
TLSSkipVerify bool
|
TLSSkipVerify *bool
|
||||||
TLSSkipClientVerify bool
|
TLSSkipClientVerify *bool
|
||||||
AzureApplicationID string
|
Status *int
|
||||||
AzureTenantID string
|
AzureApplicationID *string
|
||||||
AzureAuthenticationKey string
|
AzureTenantID *string
|
||||||
|
AzureAuthenticationKey *string
|
||||||
Tags []string
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,36 +54,49 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Name != "" {
|
if payload.Name != nil {
|
||||||
endpoint.Name = payload.Name
|
endpoint.Name = *payload.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.URL != "" {
|
if payload.URL != nil {
|
||||||
endpoint.URL = payload.URL
|
endpoint.URL = *payload.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.PublicURL != "" {
|
if payload.PublicURL != nil {
|
||||||
endpoint.PublicURL = payload.PublicURL
|
endpoint.PublicURL = *payload.PublicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.GroupID != 0 {
|
if payload.GroupID != nil {
|
||||||
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
|
endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.Tags != nil {
|
if payload.Tags != nil {
|
||||||
endpoint.Tags = payload.Tags
|
endpoint.Tags = payload.Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Status != nil {
|
||||||
|
switch *payload.Status {
|
||||||
|
case 1:
|
||||||
|
endpoint.Status = portainer.EndpointStatusUp
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
endpoint.Status = portainer.EndpointStatusDown
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
credentials := endpoint.AzureCredentials
|
credentials := endpoint.AzureCredentials
|
||||||
if payload.AzureApplicationID != "" {
|
if payload.AzureApplicationID != nil {
|
||||||
credentials.ApplicationID = payload.AzureApplicationID
|
credentials.ApplicationID = *payload.AzureApplicationID
|
||||||
}
|
}
|
||||||
if payload.AzureTenantID != "" {
|
if payload.AzureTenantID != nil {
|
||||||
credentials.TenantID = payload.AzureTenantID
|
credentials.TenantID = *payload.AzureTenantID
|
||||||
}
|
}
|
||||||
if payload.AzureAuthenticationKey != "" {
|
if payload.AzureAuthenticationKey != nil {
|
||||||
credentials.AuthenticationKey = payload.AzureAuthenticationKey
|
credentials.AuthenticationKey = *payload.AzureAuthenticationKey
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient := client.NewHTTPClient()
|
httpClient := client.NewHTTPClient()
|
||||||
|
@ -93,44 +107,55 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
endpoint.AzureCredentials = credentials
|
endpoint.AzureCredentials = credentials
|
||||||
}
|
}
|
||||||
|
|
||||||
folder := strconv.Itoa(endpointID)
|
if payload.TLS != nil {
|
||||||
if payload.TLS {
|
folder := strconv.Itoa(endpointID)
|
||||||
endpoint.TLSConfig.TLS = true
|
|
||||||
endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify
|
if *payload.TLS {
|
||||||
if !payload.TLSSkipVerify {
|
endpoint.TLSConfig.TLS = true
|
||||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
if payload.TLSSkipVerify != nil {
|
||||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
endpoint.TLSConfig.TLSSkipVerify = *payload.TLSSkipVerify
|
||||||
} else {
|
|
||||||
endpoint.TLSConfig.TLSCACertPath = ""
|
if !*payload.TLSSkipVerify {
|
||||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||||
}
|
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||||
|
} else {
|
||||||
|
endpoint.TLSConfig.TLSCACertPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.TLSSkipClientVerify != nil {
|
||||||
|
if !*payload.TLSSkipClientVerify {
|
||||||
|
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSConfig.TLSCertPath = certPath
|
||||||
|
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||||
|
} else {
|
||||||
|
endpoint.TLSConfig.TLSCertPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !payload.TLSSkipClientVerify {
|
|
||||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
|
||||||
endpoint.TLSConfig.TLSCertPath = certPath
|
|
||||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
|
||||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
|
||||||
} else {
|
} else {
|
||||||
|
endpoint.TLSConfig.TLS = false
|
||||||
|
endpoint.TLSConfig.TLSSkipVerify = false
|
||||||
|
endpoint.TLSConfig.TLSCACertPath = ""
|
||||||
endpoint.TLSConfig.TLSCertPath = ""
|
endpoint.TLSConfig.TLSCertPath = ""
|
||||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
|
||||||
endpoint.TLSConfig.TLSKeyPath = ""
|
endpoint.TLSConfig.TLSKeyPath = ""
|
||||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
err = handler.FileService.DeleteTLSFiles(folder)
|
||||||
}
|
if err != nil {
|
||||||
} else {
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||||
endpoint.TLSConfig.TLS = false
|
}
|
||||||
endpoint.TLSConfig.TLSSkipVerify = false
|
|
||||||
endpoint.TLSConfig.TLSCACertPath = ""
|
|
||||||
endpoint.TLSConfig.TLSCertPath = ""
|
|
||||||
endpoint.TLSConfig.TLSKeyPath = ""
|
|
||||||
err = handler.FileService.DeleteTLSFiles(folder)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||||
if err != nil {
|
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
|
|
@ -245,17 +245,28 @@ type (
|
||||||
|
|
||||||
// Snapshot represents a snapshot of a specific endpoint at a specific time
|
// Snapshot represents a snapshot of a specific endpoint at a specific time
|
||||||
Snapshot struct {
|
Snapshot struct {
|
||||||
Time int64 `json:"Time"`
|
Time int64 `json:"Time"`
|
||||||
DockerVersion string `json:"DockerVersion"`
|
DockerVersion string `json:"DockerVersion"`
|
||||||
Swarm bool `json:"Swarm"`
|
Swarm bool `json:"Swarm"`
|
||||||
TotalCPU int `json:"TotalCPU"`
|
TotalCPU int `json:"TotalCPU"`
|
||||||
TotalMemory int64 `json:"TotalMemory"`
|
TotalMemory int64 `json:"TotalMemory"`
|
||||||
RunningContainerCount int `json:"RunningContainerCount"`
|
RunningContainerCount int `json:"RunningContainerCount"`
|
||||||
StoppedContainerCount int `json:"StoppedContainerCount"`
|
StoppedContainerCount int `json:"StoppedContainerCount"`
|
||||||
VolumeCount int `json:"VolumeCount"`
|
VolumeCount int `json:"VolumeCount"`
|
||||||
ImageCount int `json:"ImageCount"`
|
ImageCount int `json:"ImageCount"`
|
||||||
ServiceCount int `json:"ServiceCount"`
|
ServiceCount int `json:"ServiceCount"`
|
||||||
StackCount int `json:"StackCount"`
|
StackCount int `json:"StackCount"`
|
||||||
|
SnapshotRaw SnapshotRaw `json:"SnapshotRaw"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||||
|
SnapshotRaw struct {
|
||||||
|
Containers interface{} `json:"Containers"`
|
||||||
|
Volumes interface{} `json:"Volumes"`
|
||||||
|
Networks interface{} `json:"Networks"`
|
||||||
|
Images interface{} `json:"Images"`
|
||||||
|
Info interface{} `json:"Info"`
|
||||||
|
Version interface{} `json:"Version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointGroupID represents an endpoint group identifier
|
// EndpointGroupID represents an endpoint group identifier
|
||||||
|
|
|
@ -17,6 +17,7 @@ angular.module('portainer')
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
$httpProvider.interceptors.push('jwtInterceptor');
|
$httpProvider.interceptors.push('jwtInterceptor');
|
||||||
|
$httpProvider.interceptors.push('EndpointStatusInterceptor');
|
||||||
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json';
|
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json';
|
||||||
$httpProvider.defaults.headers.put['Content-Type'] = 'application/json';
|
$httpProvider.defaults.headers.put['Content-Type'] = 'application/json';
|
||||||
$httpProvider.defaults.headers.patch['Content-Type'] = 'application/json';
|
$httpProvider.defaults.headers.patch['Content-Type'] = 'application/json';
|
||||||
|
@ -48,6 +49,7 @@ angular.module('portainer')
|
||||||
|
|
||||||
cfpLoadingBarProvider.includeSpinner = false;
|
cfpLoadingBarProvider.includeSpinner = false;
|
||||||
cfpLoadingBarProvider.parentSelector = '#loadingbar-placeholder';
|
cfpLoadingBarProvider.parentSelector = '#loadingbar-placeholder';
|
||||||
|
cfpLoadingBarProvider.latencyThreshold = 600;
|
||||||
|
|
||||||
$urlRouterProvider.otherwise('/auth');
|
$urlRouterProvider.otherwise('/auth');
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<containers-datatable-actions
|
<containers-datatable-actions ng-if="!$ctrl.offlineMode"
|
||||||
selected-items="$ctrl.state.selectedItems"
|
selected-items="$ctrl.state.selectedItems"
|
||||||
selected-item-count="$ctrl.state.selectedItemCount"
|
selected-item-count="$ctrl.state.selectedItemCount"
|
||||||
no-stopped-items-selected="$ctrl.state.noStoppedItemsSelected"
|
no-stopped-items-selected="$ctrl.state.noStoppedItemsSelected"
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -210,17 +210,18 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.containers.container({ id: item.Id, nodeName: item.NodeName })" title="{{ item | containername }}">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.containers.container({ id: item.Id, nodeName: item.NodeName })" title="{{ item | containername }}">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
|
||||||
|
<span ng-if="$ctrl.offlineMode">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td ng-show="$ctrl.columnVisibility.columns.state.display">
|
<td ng-show="$ctrl.columnVisibility.columns.state.display">
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect" ng-show="$ctrl.columnVisibility.columns.actions.display">
|
<td ng-if="!$ctrl.offlineMode && ($ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect)" ng-show="$ctrl.columnVisibility.columns.actions.display">
|
||||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
||||||
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id, nodeName: item.NodeName})" title="Stats"><i class="fa fa-chart-area space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionStats" style="margin: 0 2.5px;" ui-sref="docker.containers.container.stats({id: item.Id, nodeName: item.NodeName})" title="Stats"><i class="fa fa-chart-area space-right" aria-hidden="true"></i></a>
|
||||||
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id, nodeName: item.NodeName})" title="Logs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionLogs" style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({id: item.Id, nodeName: item.NodeName})" title="Logs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
||||||
|
@ -228,8 +229,13 @@
|
||||||
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id, nodeName: item.NodeName})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
<a ng-if="$ctrl.settings.showQuickActionInspect" style="margin: 0 2.5px;" ui-sref="docker.containers.container.inspect({id: item.Id, nodeName: item.NodeName})" title="Inspect"><i class="fa fa-info-circle space-right" aria-hidden="true"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td ng-if="$ctrl.offlineMode">
|
||||||
|
</td>
|
||||||
<td ng-show="$ctrl.columnVisibility.columns.stack.display">{{ item.StackName ? item.StackName : '-' }}</td>
|
<td ng-show="$ctrl.columnVisibility.columns.stack.display">{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
<td ng-show="$ctrl.columnVisibility.columns.image.display"><a ui-sref="docker.images.image({ id: item.Image })">{{ item.Image | trimshasum }}</a></td>
|
<td ng-show="$ctrl.columnVisibility.columns.image.display">
|
||||||
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.images.image({ id: item.Image })">{{ item.Image | trimshasum }}</a>
|
||||||
|
<span ng-if="$ctrl.offlineMode">{{ item.Image | trimshasum }}</span>
|
||||||
|
</td>
|
||||||
<td ng-show="$ctrl.columnVisibility.columns.created.display">
|
<td ng-show="$ctrl.columnVisibility.columns.created.display">
|
||||||
{{item.Created | getisodatefromtimestamp}}
|
{{item.Created | getisodatefromtimestamp}}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('containersDatatable', {
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
showAddAction: '<'
|
showAddAction: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -204,7 +204,6 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
setDefaults(this);
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
this.state.reverseOrder = storedOrder.reverse;
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems, false)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems, false)">
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.usage.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.usage.open">
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -111,11 +111,12 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.images.image({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.images.image({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
||||||
|
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span>
|
||||||
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::item.ContainerCount === 0">Unused</span>
|
<span style="margin-left: 10px;" class="label label-warning image-tag" ng-if="::item.ContainerCount === 0">Unused</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -12,6 +12,7 @@ angular.module('portainer.docker').component('imagesDatatable', {
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
downloadAction: '<',
|
downloadAction: '<',
|
||||||
forceRemoveAction: '<',
|
forceRemoveAction: '<',
|
||||||
exportInProgress: '<'
|
exportInProgress: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -109,11 +109,12 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||||
|
<span ng-if="$ctrl.offlineMode">{{ item.Name | truncate:40 }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
<td>{{ item.Scope }}</td>
|
<td>{{ item.Scope }}</td>
|
||||||
|
|
|
@ -10,6 +10,7 @@ angular.module('portainer.docker').component('networksDatatable', {
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
removeAction: '<'
|
removeAction: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -98,12 +98,13 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
||||||
<a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left" ng-if="$ctrl.showBrowseAction">
|
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span>
|
||||||
|
<a ui-sref="docker.volumes.volume.browse({ id: item.Id, nodeName: item.NodeName })" class="btn btn-xs btn-primary space-left" ng-if="$ctrl.showBrowseAction && !$ctrl.offlineMode">
|
||||||
<i class="fa fa-search"></i> browse</a>
|
<i class="fa fa-search"></i> browse</a>
|
||||||
</a>
|
</a>
|
||||||
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
|
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
|
||||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer.docker').component('volumesDatatable', {
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
showBrowseAction: '<'
|
showBrowseAction: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ angular.module('portainer.docker').component('dockerSidebarContent', {
|
||||||
endpointApiVersion: '<',
|
endpointApiVersion: '<',
|
||||||
swarmManagement: '<',
|
swarmManagement: '<',
|
||||||
standaloneManagement: '<',
|
standaloneManagement: '<',
|
||||||
adminAccess: '<'
|
adminAccess: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list" ng-if="!$ctrl.offlineMode">
|
||||||
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
|
<li class="sidebar-list" ng-if="$ctrl.endpointApiVersion >= 1.25 && $ctrl.swarmManagement">
|
||||||
<a ui-sref="docker.secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
|
<a ui-sref="docker.secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess">
|
<li class="sidebar-list" ng-if="$ctrl.standaloneManagement && $ctrl.adminAccess && !$ctrl.offlineMode">
|
||||||
<a ui-sref="docker.events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
|
<a ui-sref="docker.events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
||||||
|
|
|
@ -8,25 +8,27 @@
|
||||||
<rd-header-content>Docker</rd-header-content>
|
<rd-header-content>Docker</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
<information-panel-offline ng-if="$ctrl.offlineMode"></information-panel-offline>
|
||||||
|
|
||||||
<host-details-panel
|
<host-details-panel
|
||||||
host="$ctrl.hostDetails"
|
host="$ctrl.hostDetails"
|
||||||
is-browse-enabled="$ctrl.isAgent && $ctrl.agentApiVersion > 1"
|
is-browse-enabled="$ctrl.isAgent && $ctrl.agentApiVersion > 1 && !$ctrl.offlineMode"
|
||||||
browse-url="{{$ctrl.browseUrl}}"
|
browse-url="{{$ctrl.browseUrl}}"
|
||||||
is-job-enabled="$ctrl.isJobEnabled"
|
is-job-enabled="$ctrl.isJobEnabled && !$ctrl.offlineMode"
|
||||||
job-url="{{$ctrl.jobUrl}}"
|
job-url="{{$ctrl.jobUrl}}"
|
||||||
></host-details-panel>
|
></host-details-panel>
|
||||||
|
|
||||||
<engine-details-panel engine="$ctrl.engineDetails"></engine-details-panel>
|
<engine-details-panel engine="$ctrl.engineDetails"></engine-details-panel>
|
||||||
|
|
||||||
<jobs-datatable
|
<jobs-datatable
|
||||||
ng-if="$ctrl.isJobEnabled && $ctrl.jobs"
|
ng-if="$ctrl.isJobEnabled && $ctrl.jobs && !$ctrl.offlineMode"
|
||||||
title-text="Jobs" title-icon="fa-tasks"
|
title-text="Jobs" title-icon="fa-tasks"
|
||||||
dataset="$ctrl.jobs"
|
dataset="$ctrl.jobs"
|
||||||
table-key="jobs"
|
table-key="jobs"
|
||||||
order-by="Created" reverse-order="true"
|
order-by="Created" reverse-order="true"
|
||||||
></jobs-datatable>
|
></jobs-datatable>
|
||||||
|
|
||||||
<devices-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1" devices="$ctrl.devices"></devices-panel>
|
<devices-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1 && !$ctrl.offlineMode" devices="$ctrl.devices"></devices-panel>
|
||||||
<disks-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1" disks="$ctrl.disks"></disks-panel>
|
<disks-panel ng-if="$ctrl.isAgent && $ctrl.agentApiVersion > 1 && !$ctrl.offlineMode" disks="$ctrl.disks"></disks-panel>
|
||||||
|
|
||||||
<ng-transclude></ng-transclude>
|
<ng-transclude></ng-transclude>
|
||||||
|
|
|
@ -6,6 +6,7 @@ angular.module('portainer.docker').component('hostOverview', {
|
||||||
devices: '<',
|
devices: '<',
|
||||||
disks: '<',
|
disks: '<',
|
||||||
isAgent: '<',
|
isAgent: '<',
|
||||||
|
offlineMode: '<',
|
||||||
agentApiVersion: '<',
|
agentApiVersion: '<',
|
||||||
refreshUrl: '@',
|
refreshUrl: '@',
|
||||||
browseUrl: '@',
|
browseUrl: '@',
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('ContainersInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Containers;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('ImagesInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Images;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('InfoInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Info;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('NetworksInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Networks;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('VersionInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Version;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -0,0 +1,21 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('VolumesInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
if (rejection.status === 502 || rejection.status === -1) {
|
||||||
|
var endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined) {
|
||||||
|
var data = endpoint.Snapshots[0].SnapshotRaw.Volumes;
|
||||||
|
if (data !== undefined) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
||||||
function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, ContainersInterceptor) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', {
|
||||||
name: '@name',
|
name: '@name',
|
||||||
|
@ -9,7 +9,7 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
||||||
isArray: true
|
isArray: true, interceptor: ContainersInterceptor, timeout: 10000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET', params: { action: 'json' }
|
method: 'GET', params: { action: 'json' }
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper',
|
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor',
|
||||||
function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) {
|
function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper, ImagesInterceptor) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', {
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
|
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 10000},
|
||||||
get: {method: 'GET', params: {action: 'json'}},
|
get: {method: 'GET', params: {action: 'json'}},
|
||||||
search: {method: 'GET', params: {action: 'search'}},
|
search: {method: 'GET', params: {action: 'search'}},
|
||||||
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider',
|
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor',
|
||||||
function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, NetworksInterceptor) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', {
|
||||||
id: '@id',
|
id: '@id',
|
||||||
|
@ -8,7 +8,7 @@ function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', isArray: true
|
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 10000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor',
|
||||||
|
function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, InfoInterceptor, VersionInterceptor) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', {
|
||||||
name: '@name',
|
name: '@name',
|
||||||
|
@ -7,10 +8,9 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: {
|
info: {
|
||||||
method: 'GET', params: { action: 'info' },
|
method: 'GET', params: { action: 'info' }, timeout: 10000, interceptor: InfoInterceptor
|
||||||
ignoreLoadingBar: true
|
|
||||||
},
|
},
|
||||||
version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true, timeout: 4500 },
|
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
||||||
events: {
|
events: {
|
||||||
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
|
method: 'GET', params: { action: 'events', since: '@since', until: '@until' },
|
||||||
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular.module('portainer.docker')
|
||||||
|
.factory('SystemEndpoint', ['$resource', 'API_ENDPOINT_ENDPOINTS',
|
||||||
|
function SystemEndpointFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', {
|
||||||
|
name: '@name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ping: {
|
||||||
|
method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }, timeout: 10000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -1,12 +1,13 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor',
|
||||||
|
function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, VolumesInterceptor) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action',
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action',
|
||||||
{
|
{
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: { method: 'GET' },
|
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 10000},
|
||||||
get: { method: 'GET', params: {id: '@id'} },
|
get: { method: 'GET', params: {id: '@id'} },
|
||||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
||||||
remove: {
|
remove: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('SystemService', ['$q', 'System', function SystemServiceFactory($q, System) {
|
.factory('SystemService', ['$q', 'System', 'SystemEndpoint', function SystemServiceFactory($q, System, SystemEndpoint) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
@ -20,6 +20,10 @@ angular.module('portainer.docker')
|
||||||
return System.info({}).$promise;
|
return System.info({}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.ping = function(endpointId) {
|
||||||
|
return SystemEndpoint.ping({endpointId: endpointId}).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
service.version = function() {
|
service.version = function() {
|
||||||
return System.version({}).$promise;
|
return System.version({}).$promise;
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>Containers</rd-header-content>
|
<rd-header-content>Containers</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12" ng-if="containers">
|
<div class="col-sm-12" ng-if="containers">
|
||||||
<containers-datatable
|
<containers-datatable
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||||
show-add-action="true"
|
show-add-action="true"
|
||||||
|
offline-mode="offlineMode"
|
||||||
></containers-datatable>
|
></containers-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainersController', ['$scope', 'ContainerService', 'Notifications',
|
.controller('ContainersController', ['$scope', 'ContainerService', 'Notifications', 'EndpointProvider',
|
||||||
function ($scope, ContainerService, Notifications) {
|
function ($scope, ContainerService, Notifications, EndpointProvider) {
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
ContainerService.containers(1)
|
ContainerService.containers(1)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.containers = data;
|
$scope.containers = data;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
Notifications.error('Failure', err, 'Unable to retrieve containers');
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<dashboard-cluster-agent-info></dashboard-cluster-agent-info>
|
<dashboard-cluster-agent-info></dashboard-cluster-agent-info>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<information-panel
|
<information-panel
|
||||||
ng-if="!applicationState.UI.dismissedInfoPanels['docker-dashboard-info-01'] && !applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
ng-if="!applicationState.UI.dismissedInfoPanels['docker-dashboard-info-01'] && !applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||||
title-text="Information"
|
title-text="Information"
|
||||||
|
|
|
@ -6,6 +6,8 @@ function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeServ
|
||||||
StateManager.dismissInformationPanel(id);
|
StateManager.dismissInformationPanel(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
var endpointId = EndpointProvider.endpointID();
|
var endpointId = EndpointProvider.endpointID();
|
||||||
|
@ -29,6 +31,7 @@ function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeServ
|
||||||
$scope.stackCount = data.stacks.length;
|
$scope.stackCount = data.stacks.length;
|
||||||
$scope.info = data.info;
|
$scope.info = data.info;
|
||||||
$scope.endpoint = data.endpoint;
|
$scope.endpoint = data.endpoint;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to load dashboard data');
|
Notifications.error('Failure', err, 'Unable to load dashboard data');
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
angular.module('portainer.docker').controller('HostViewController', [
|
angular.module('portainer.docker').controller('HostViewController', [
|
||||||
'$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService', 'ContainerService', 'Authentication',
|
'$q', 'SystemService', 'Notifications', 'StateManager', 'AgentService', 'ContainerService', 'Authentication', 'EndpointProvider',
|
||||||
function HostViewController($q, SystemService, Notifications, StateManager, AgentService, ContainerService, Authentication) {
|
function HostViewController($q, SystemService, Notifications, StateManager, AgentService, ContainerService, Authentication, EndpointProvider) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.$onInit = initView;
|
this.$onInit = initView;
|
||||||
|
|
||||||
ctrl.state = {
|
ctrl.state = {
|
||||||
isAgent: false,
|
isAgent: false,
|
||||||
isAdmin : false
|
isAdmin : false,
|
||||||
|
offlineMode: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.engineDetails = {};
|
this.engineDetails = {};
|
||||||
|
@ -30,6 +31,7 @@ angular.module('portainer.docker').controller('HostViewController', [
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
ctrl.engineDetails = buildEngineDetails(data);
|
ctrl.engineDetails = buildEngineDetails(data);
|
||||||
ctrl.hostDetails = buildHostDetails(data.info);
|
ctrl.hostDetails = buildHostDetails(data.info);
|
||||||
|
ctrl.state.offlineMode = EndpointProvider.offlineMode();
|
||||||
ctrl.jobs = data.jobs;
|
ctrl.jobs = data.jobs;
|
||||||
|
|
||||||
if (ctrl.state.isAgent && agentApiVersion > 1) {
|
if (ctrl.state.isAgent && agentApiVersion > 1) {
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
devices="$ctrl.devices"
|
devices="$ctrl.devices"
|
||||||
refresh-url="docker.host"
|
refresh-url="docker.host"
|
||||||
browse-url="docker.host.browser"
|
browse-url="docker.host.browser"
|
||||||
is-job-enabled="$ctrl.state.isAdmin"
|
offline-mode="$ctrl.state.offlineMode"
|
||||||
|
is-job-enabled="$ctrl.state.isAdmin && !$ctrl.state.offlineMode"
|
||||||
job-url="docker.host.job"
|
job-url="docker.host.job"
|
||||||
jobs="$ctrl.jobs"
|
jobs="$ctrl.jobs"
|
||||||
></host-overview>
|
></host-overview>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<rd-header-content>Images</rd-header-content>
|
<rd-header-content>Images</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" ng-if="!offlineMode">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-download" title-text="Pull image ">
|
<rd-widget-header icon="fa-download" title-text="Pull image ">
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<images-datatable
|
<images-datatable
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
force-remove-action="confirmRemovalAction"
|
force-remove-action="confirmRemovalAction"
|
||||||
export-in-progress="state.exportInProgress"
|
export-in-progress="state.exportInProgress"
|
||||||
|
offline-mode="offlineMode"
|
||||||
></images-datatable>
|
></images-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob',
|
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'ModalService', 'HttpRequestHelper', 'FileSaver', 'Blob', 'EndpointProvider',
|
||||||
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob) {
|
function ($scope, $state, ImageService, Notifications, ModalService, HttpRequestHelper, FileSaver, Blob, EndpointProvider) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
exportInProgress: false
|
exportInProgress: false
|
||||||
|
@ -113,10 +113,13 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
ImageService.images(true)
|
ImageService.images(true)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.images = data;
|
$scope.images = data;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve images');
|
Notifications.error('Failure', err, 'Unable to retrieve images');
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>Networks</rd-header-content>
|
<rd-header-content>Networks</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<networks-datatable
|
<networks-datatable
|
||||||
|
@ -16,6 +16,7 @@
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||||
|
offline-mode="offlineMode"
|
||||||
></networks-datatable>
|
></networks-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper',
|
.controller('NetworksController', ['$scope', '$state', 'NetworkService', 'Notifications', 'HttpRequestHelper', 'EndpointProvider',
|
||||||
function ($scope, $state, NetworkService, Notifications, HttpRequestHelper) {
|
function ($scope, $state, NetworkService, Notifications, HttpRequestHelper, EndpointProvider) {
|
||||||
|
|
||||||
$scope.removeAction = function (selectedItems) {
|
$scope.removeAction = function (selectedItems) {
|
||||||
var actionCount = selectedItems.length;
|
var actionCount = selectedItems.length;
|
||||||
|
@ -24,10 +24,13 @@ function ($scope, $state, NetworkService, Notifications, HttpRequestHelper) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
NetworkService.networks(true, true, true)
|
NetworkService.networks(true, true, true)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.networks = data;
|
$scope.networks = data;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.networks = [];
|
$scope.networks = [];
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>Volumes</rd-header-content>
|
<rd-header-content>Volumes</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<volumes-datatable
|
<volumes-datatable
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||||
show-browse-action="applicationState.endpoint.mode.agentProxy"
|
show-browse-action="applicationState.endpoint.mode.agentProxy"
|
||||||
|
offline-mode="offlineMode"
|
||||||
></volumes-datatable>
|
></volumes-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper',
|
.controller('VolumesController', ['$q', '$scope', '$state', 'VolumeService', 'ServiceService', 'VolumeHelper', 'Notifications', 'HttpRequestHelper', 'EndpointProvider',
|
||||||
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper) {
|
function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notifications, HttpRequestHelper, EndpointProvider) {
|
||||||
|
|
||||||
$scope.removeAction = function (selectedItems) {
|
$scope.removeAction = function (selectedItems) {
|
||||||
var actionCount = selectedItems.length;
|
var actionCount = selectedItems.length;
|
||||||
|
@ -24,6 +24,8 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||||
var endpointRole = $scope.applicationState.endpoint.mode.role;
|
var endpointRole = $scope.applicationState.endpoint.mode.role;
|
||||||
|
@ -35,6 +37,7 @@ function ($q, $scope, $state, VolumeService, ServiceService, VolumeHelper, Notif
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var services = data.services;
|
var services = data.services;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
$scope.volumes = data.attached.map(function(volume) {
|
$scope.volumes = data.attached.map(function(volume) {
|
||||||
volume.dangling = false;
|
volume.dangling = false;
|
||||||
return volume;
|
return volume;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -54,11 +54,12 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="item.External && item.Type === 2"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="item.External && item.Type === 2"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="portainer.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, external: item.External })">{{ item.Name }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="portainer.stacks.stack({ name: item.Name, id: item.Id, type: item.Type, external: item.External })">{{ item.Name }}</a>
|
||||||
|
<span ng-if="$ctrl.offlineMode">{{ item.Name }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
|
<td>{{ item.Type === 1 ? 'Swarm' : 'Compose' }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.app').component('stacksDatatable', {
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
removeAction: '<'
|
removeAction: '<',
|
||||||
|
offlineMode: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
<information-panel title-text="Offline mode" cant-dismiss="true">
|
||||||
|
<span class="small">
|
||||||
|
<p class="text-muted">
|
||||||
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
|
@ -0,0 +1,4 @@
|
||||||
|
angular.module('portainer.app').component('informationPanelOffline', {
|
||||||
|
templateUrl: 'app/portainer/components/information-panel-offline/informationPanelOffline.html',
|
||||||
|
transclude: true
|
||||||
|
});
|
|
@ -2,7 +2,7 @@ angular.module('portainer.app').component('informationPanel', {
|
||||||
templateUrl: 'app/portainer/components/information-panel/informationPanel.html',
|
templateUrl: 'app/portainer/components/information-panel/informationPanel.html',
|
||||||
bindings: {
|
bindings: {
|
||||||
titleText: '@',
|
titleText: '@',
|
||||||
dismissAction: '&'
|
dismissAction: '&?'
|
||||||
},
|
},
|
||||||
transclude: true
|
transclude: true
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<span style="float: left;">
|
<span style="float: left;">
|
||||||
{{ $ctrl.titleText }}
|
{{ $ctrl.titleText }}
|
||||||
</span>
|
</span>
|
||||||
<span class="small" style="float: right;">
|
<span class="small" style="float: right;" ng-if="$ctrl.dismissAction">
|
||||||
<a ng-click="$ctrl.dismissAction()"><i class="fa fa-times"></i> dismiss</a>
|
<a ng-click="$ctrl.dismissAction()"><i class="fa fa-times"></i> dismiss</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.factory('EndpointStatusInterceptor', ['$q', '$injector', 'EndpointProvider', function ($q, $injector, EndpointProvider) {
|
||||||
|
'use strict';
|
||||||
|
var interceptor = {};
|
||||||
|
|
||||||
|
interceptor.response = responseInterceptor;
|
||||||
|
interceptor.responseError = responseErrorInterceptor;
|
||||||
|
|
||||||
|
function canBeOffline(url) {
|
||||||
|
return (_.startsWith(url, 'api/') && (
|
||||||
|
_.includes(url, '/containers') ||
|
||||||
|
_.includes(url, '/images') ||
|
||||||
|
_.includes(url, '/volumes') ||
|
||||||
|
_.includes(url, '/networks') ||
|
||||||
|
_.includes(url, '/info') ||
|
||||||
|
_.includes(url, '/version')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseInterceptor(response) {
|
||||||
|
var EndpointService = $injector.get('EndpointService');
|
||||||
|
var url = response.config.url;
|
||||||
|
if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) {
|
||||||
|
EndpointProvider.setOfflineMode(false);
|
||||||
|
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(false)});
|
||||||
|
}
|
||||||
|
return response || $q.when(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseErrorInterceptor(rejection) {
|
||||||
|
var EndpointService = $injector.get('EndpointService');
|
||||||
|
var url = rejection.config.url;
|
||||||
|
if ((rejection.status === 502 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||||
|
EndpointProvider.setOfflineMode(true);
|
||||||
|
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)});
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interceptor;
|
||||||
|
}]);
|
|
@ -7,19 +7,31 @@ angular.module('portainer.app')
|
||||||
service.initialize = function() {
|
service.initialize = function() {
|
||||||
var endpointID = LocalStorage.getEndpointID();
|
var endpointID = LocalStorage.getEndpointID();
|
||||||
var endpointPublicURL = LocalStorage.getEndpointPublicURL();
|
var endpointPublicURL = LocalStorage.getEndpointPublicURL();
|
||||||
|
var offlineMode = LocalStorage.getOfflineMode();
|
||||||
|
|
||||||
if (endpointID) {
|
if (endpointID) {
|
||||||
endpoint.ID = endpointID;
|
endpoint.ID = endpointID;
|
||||||
}
|
}
|
||||||
if (endpointPublicURL) {
|
if (endpointPublicURL) {
|
||||||
endpoint.PublicURL = endpointPublicURL;
|
endpoint.PublicURL = endpointPublicURL;
|
||||||
}
|
}
|
||||||
|
if (offlineMode) {
|
||||||
|
endpoint.OfflineMode = offlineMode;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
service.clean = function() {
|
service.clean = function() {
|
||||||
endpoint = {};
|
endpoint = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.endpoint = function() {
|
||||||
|
return endpoint;
|
||||||
|
};
|
||||||
|
|
||||||
service.endpointID = function() {
|
service.endpointID = function() {
|
||||||
|
if (endpoint.ID === undefined) {
|
||||||
|
endpoint.ID = LocalStorage.getEndpointID();
|
||||||
|
}
|
||||||
return endpoint.ID;
|
return endpoint.ID;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +41,9 @@ angular.module('portainer.app')
|
||||||
};
|
};
|
||||||
|
|
||||||
service.endpointPublicURL = function() {
|
service.endpointPublicURL = function() {
|
||||||
|
if (endpoint.PublicURL === undefined) {
|
||||||
|
endpoint.PublicURL = LocalStorage.getEndpointPublicURL();
|
||||||
|
}
|
||||||
return endpoint.PublicURL;
|
return endpoint.PublicURL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,5 +52,40 @@ angular.module('portainer.app')
|
||||||
LocalStorage.storeEndpointPublicURL(publicURL);
|
LocalStorage.storeEndpointPublicURL(publicURL);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.endpoints = function() {
|
||||||
|
return LocalStorage.getEndpoints();
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setEndpoints = function(data) {
|
||||||
|
LocalStorage.storeEndpoints(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
service.offlineMode = function() {
|
||||||
|
return endpoint.OfflineMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.endpointStatusFromOfflineMode = function(isOffline) {
|
||||||
|
return isOffline ? 2 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setOfflineMode = function(isOffline) {
|
||||||
|
endpoint.OfflineMode = isOffline;
|
||||||
|
LocalStorage.storeOfflineMode(isOffline);
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setOfflineModeFromStatus = function(status) {
|
||||||
|
var isOffline = status !== 1;
|
||||||
|
endpoint.OfflineMode = isOffline;
|
||||||
|
LocalStorage.storeOfflineMode(isOffline);
|
||||||
|
};
|
||||||
|
|
||||||
|
service.currentEndpoint = function() {
|
||||||
|
var endpointId = endpoint.ID;
|
||||||
|
var endpoints = LocalStorage.getEndpoints();
|
||||||
|
return _.find(endpoints, function (item) {
|
||||||
|
return item.Id === endpointId;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -4,9 +4,14 @@ function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionServ
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.initEndpointExtensions = function() {
|
service.initEndpointExtensions = function(endpoint) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
if (endpoint.Status !== 1) {
|
||||||
|
deferred.resolve([]);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
SystemService.version()
|
SystemService.version()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpointAPIVersion = parseFloat(data.ApiVersion);
|
var endpointAPIVersion = parseFloat(data.ApiVersion);
|
||||||
|
|
|
@ -14,6 +14,18 @@ angular.module('portainer.app')
|
||||||
getEndpointPublicURL: function() {
|
getEndpointPublicURL: function() {
|
||||||
return localStorageService.get('ENDPOINT_PUBLIC_URL');
|
return localStorageService.get('ENDPOINT_PUBLIC_URL');
|
||||||
},
|
},
|
||||||
|
storeOfflineMode: function(isOffline) {
|
||||||
|
localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline);
|
||||||
|
},
|
||||||
|
getOfflineMode: function() {
|
||||||
|
return localStorageService.get('ENDPOINT_OFFLINE_MODE');
|
||||||
|
},
|
||||||
|
storeEndpoints: function(data) {
|
||||||
|
localStorageService.set('ENDPOINTS_DATA', data);
|
||||||
|
},
|
||||||
|
getEndpoints: function() {
|
||||||
|
return localStorageService.get('ENDPOINTS_DATA');
|
||||||
|
},
|
||||||
storeEndpointState: function(state) {
|
storeEndpointState: function(state) {
|
||||||
localStorageService.set('ENDPOINT_STATE', state);
|
localStorageService.set('ENDPOINT_STATE', state);
|
||||||
},
|
},
|
||||||
|
|
|
@ -135,11 +135,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.updateEndpointState = function(name, type, extensions) {
|
manager.updateEndpointState = function(endpoint, extensions) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
if (type === 3) {
|
if (endpoint.Type === 3) {
|
||||||
state.endpoint.name = name;
|
state.endpoint.name = endpoint.Name;
|
||||||
state.endpoint.mode = { provider: 'AZURE' };
|
state.endpoint.mode = { provider: 'AZURE' };
|
||||||
LocalStorage.storeEndpointState(state.endpoint);
|
LocalStorage.storeEndpointState(state.endpoint);
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
@ -147,23 +147,23 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
}
|
}
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
version: SystemService.version(),
|
version: endpoint.Status === 1 ? SystemService.version() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Version),
|
||||||
info: SystemService.info()
|
info: endpoint.Status === 1 ? SystemService.info() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Info)
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpointMode = InfoHelper.determineEndpointMode(data.info, type);
|
var endpointMode = InfoHelper.determineEndpointMode(data.info, endpoint.Type);
|
||||||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||||
state.endpoint.mode = endpointMode;
|
state.endpoint.mode = endpointMode;
|
||||||
state.endpoint.name = name;
|
state.endpoint.name = endpoint.Name;
|
||||||
state.endpoint.apiVersion = endpointAPIVersion;
|
state.endpoint.apiVersion = endpointAPIVersion;
|
||||||
state.endpoint.extensions = assignExtensions(extensions);
|
state.endpoint.extensions = assignExtensions(extensions);
|
||||||
|
|
||||||
if (endpointMode.agentProxy) {
|
if (endpointMode.agentProxy && endpoint.Status === 1) {
|
||||||
return AgentPingService.ping().then(function onPingSuccess(data) {
|
return AgentPingService.ping().then(function onPingSuccess(data) {
|
||||||
state.endpoint.agentApiVersion = data.version;
|
state.endpoint.agentApiVersion = data.version;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
LocalStorage.storeEndpointState(state.endpoint);
|
LocalStorage.storeEndpointState(state.endpoint);
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
|
|
@ -1,44 +1,71 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService',
|
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService', 'SystemService',
|
||||||
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService) {
|
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService, SystemService) {
|
||||||
|
|
||||||
$scope.goToDashboard = function(endpoint) {
|
$scope.goToEdit = function(id) {
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
$state.go('portainer.endpoints.endpoint', { id: id });
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
|
||||||
if (endpoint.Type === 3) {
|
|
||||||
switchToAzureEndpoint(endpoint);
|
|
||||||
} else {
|
|
||||||
switchToDockerEndpoint(endpoint);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.dismissImportantInformation = function(hash) {
|
$scope.goToDashboard = function (endpoint) {
|
||||||
|
if (endpoint.Type === 3) {
|
||||||
|
return switchToAzureEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEndpointStatus(endpoint)
|
||||||
|
.then(function sucess() {
|
||||||
|
return switchToDockerEndpoint(endpoint);
|
||||||
|
}).catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to verify endpoint status');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.dismissImportantInformation = function (hash) {
|
||||||
StateManager.dismissImportantInformation(hash);
|
StateManager.dismissImportantInformation(hash);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.dismissInformationPanel = function(id) {
|
$scope.dismissInformationPanel = function (id) {
|
||||||
StateManager.dismissInformationPanel(id);
|
StateManager.dismissInformationPanel(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
function triggerSnapshot() {
|
$scope.triggerSnapshot = function () {
|
||||||
EndpointService.snapshot()
|
|
||||||
.then(function success() {
|
|
||||||
Notifications.success('Success', 'Endpoints updated');
|
|
||||||
$state.reload();
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'An error occured during endpoint snapshot');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.triggerSnapshot = function() {
|
|
||||||
ModalService.confirmEndpointSnapshot(function (result) {
|
ModalService.confirmEndpointSnapshot(function (result) {
|
||||||
if(!result) { return; }
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
triggerSnapshot();
|
triggerSnapshot();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function checkEndpointStatus(endpoint) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
var status = 1;
|
||||||
|
SystemService.ping(endpoint.Id)
|
||||||
|
.then(function sucess() {
|
||||||
|
status = 1;
|
||||||
|
}).catch(function error() {
|
||||||
|
status = 2;
|
||||||
|
}).finally(function () {
|
||||||
|
if (endpoint.Status === status) {
|
||||||
|
deferred.resolve(endpoint);
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
|
||||||
|
.then(function sucess() {
|
||||||
|
deferred.resolve(endpoint);
|
||||||
|
}).catch(function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to update endpoint status', err: err});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
function switchToAzureEndpoint(endpoint) {
|
function switchToAzureEndpoint(endpoint) {
|
||||||
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
|
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
||||||
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
|
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
$state.go('azure.dashboard');
|
$state.go('azure.dashboard');
|
||||||
|
@ -49,10 +76,21 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchToDockerEndpoint(endpoint) {
|
function switchToDockerEndpoint(endpoint) {
|
||||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) {
|
||||||
|
Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.');
|
||||||
|
return;
|
||||||
|
} else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) {
|
||||||
|
Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
|
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
|
||||||
|
ExtensionManager.initEndpointExtensions(endpoint)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var extensions = data;
|
var extensions = data;
|
||||||
return StateManager.updateEndpointState(endpoint.Name, endpoint.Type, extensions);
|
return StateManager.updateEndpointState(endpoint, extensions);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
$state.go('docker.dashboard');
|
$state.go('docker.dashboard');
|
||||||
|
@ -62,10 +100,15 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.goToEdit = goToEdit;
|
function triggerSnapshot() {
|
||||||
|
EndpointService.snapshot()
|
||||||
function goToEdit(id) {
|
.then(function success() {
|
||||||
$state.go('portainer.endpoints.endpoint', { id: id });
|
Notifications.success('Success', 'Endpoints updated');
|
||||||
|
$state.reload();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'An error occured during endpoint snapshot');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
|
@ -85,6 +128,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
|
||||||
var groups = data.groups;
|
var groups = data.groups;
|
||||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||||
$scope.endpoints = endpoints;
|
$scope.endpoints = endpoints;
|
||||||
|
EndpointProvider.setEndpoints(endpoints);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
|
.controller('MainController', ['$scope', '$cookieStore', 'StateManager', 'EndpointProvider',
|
||||||
function ($scope, $cookieStore, StateManager) {
|
function ($scope, $cookieStore, StateManager, EndpointProvider) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sidebar Toggle & Cookie Control
|
* Sidebar Toggle & Cookie Control
|
||||||
|
@ -11,6 +11,7 @@ function ($scope, $cookieStore, StateManager) {
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.applicationState = StateManager.getState();
|
$scope.applicationState = StateManager.getState();
|
||||||
|
$scope.endpointState = EndpointProvider.endpoint();
|
||||||
|
|
||||||
$scope.$watch($scope.getWidth, function(newValue) {
|
$scope.$watch($scope.getWidth, function(newValue) {
|
||||||
if (newValue >= mobileView) {
|
if (newValue >= mobileView) {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||||
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
||||||
admin-access="!applicationState.application.authentication || isAdmin"
|
admin-access="!applicationState.application.authentication || isAdmin"
|
||||||
|
offline-mode="endpointState.OfflineMode"
|
||||||
></docker-sidebar-content>
|
></docker-sidebar-content>
|
||||||
<li class="sidebar-title" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
<li class="sidebar-title" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
||||||
<span>Extensions</span>
|
<span>Extensions</span>
|
||||||
|
|
|
@ -1,36 +1,37 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('SidebarController', ['$q', '$scope', 'StateManager', 'Notifications', 'Authentication', 'UserService',
|
.controller('SidebarController', ['$q', '$scope', 'StateManager', 'Notifications', 'Authentication', 'UserService',
|
||||||
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
|
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
|
||||||
|
|
||||||
function checkPermissions(memberships) {
|
function checkPermissions(memberships) {
|
||||||
var isLeader = false;
|
var isLeader = false;
|
||||||
angular.forEach(memberships, function(membership) {
|
angular.forEach(memberships, function (membership) {
|
||||||
if (membership.Role === 1) {
|
if (membership.Role === 1) {
|
||||||
isLeader = true;
|
isLeader = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.isTeamLeader = isLeader;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
$scope.isTeamLeader = isLeader;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.uiVersion = StateManager.getState().application.version;
|
$scope.uiVersion = StateManager.getState().application.version;
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
if (authenticationEnabled) {
|
if (authenticationEnabled) {
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
var isAdmin = userDetails.role === 1;
|
var isAdmin = userDetails.role === 1;
|
||||||
$scope.isAdmin = isAdmin;
|
$scope.isAdmin = isAdmin;
|
||||||
|
|
||||||
$q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
|
$q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
checkPermissions(data);
|
checkPermissions(data);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}
|
}
|
||||||
}
|
]);
|
||||||
|
|
||||||
initView();
|
|
||||||
}]);
|
|
|
@ -6,7 +6,7 @@
|
||||||
</rd-header-title>
|
</rd-header-title>
|
||||||
<rd-header-content>Stacks</rd-header-content>
|
<rd-header-content>Stacks</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
<information-panel-offline ng-if="offlineMode"></information-panel-offline>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<stacks-datatable
|
<stacks-datatable
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
|
offline-mode="offlineMode"
|
||||||
></stacks-datatable>
|
></stacks-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,6 +33,8 @@ function ($scope, $state, Notifications, StackService, ModalService, EndpointPro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
var endpointId = EndpointProvider.endpointID();
|
var endpointId = EndpointProvider.endpointID();
|
||||||
|
@ -45,6 +47,7 @@ function ($scope, $state, Notifications, StackService, ModalService, EndpointPro
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var stacks = data;
|
var stacks = data;
|
||||||
$scope.stacks = stacks;
|
$scope.stacks = stacks;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.stacks = [];
|
$scope.stacks = [];
|
||||||
|
|
Loading…
Reference in New Issue