mirror of https://github.com/portainer/portainer
Merge branch 'develop' into feat2240-host-view
commit
c780d52bcf
|
@ -77,14 +77,14 @@ The subject contains succinct description of the change:
|
||||||
|
|
||||||
## Contribution process
|
## Contribution process
|
||||||
|
|
||||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `contrib/` labels, such as `contrib/func-review-in-progress` or `contrib/tech-review-approved`.
|
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||||
|
|
||||||
### Bug report
|
### Bug report
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Feature request
|
### Feature request
|
||||||
|
|
||||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation.
|
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
||||||
|
|
||||||

|

|
||||||
|
|
|
@ -178,7 +178,7 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.19.2-dev
|
// Portainer 1.19.2
|
||||||
if m.currentDBVersion < 14 {
|
if m.currentDBVersion < 14 {
|
||||||
err := m.updateResourceControlsToDBVersion14()
|
err := m.updateResourceControlsToDBVersion14()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
|
errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)")
|
||||||
|
defaultHTTPTimeout = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPClient represents a client to send HTTP requests.
|
// HTTPClient represents a client to send HTTP requests.
|
||||||
|
@ -26,7 +27,7 @@ type HTTPClient struct {
|
||||||
func NewHTTPClient() *HTTPClient {
|
func NewHTTPClient() *HTTPClient {
|
||||||
return &HTTPClient{
|
return &HTTPClient{
|
||||||
&http.Client{
|
&http.Client{
|
||||||
Timeout: time.Second * 5,
|
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,10 +68,16 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get executes a simple HTTP GET to the specified URL and returns
|
// Get executes a simple HTTP GET to the specified URL and returns
|
||||||
// the content of the response body.
|
// the content of the response body. Timeout can be specified via the timeout parameter,
|
||||||
func Get(url string) ([]byte, error) {
|
// will default to defaultHTTPTimeout if set to 0.
|
||||||
|
func Get(url string, timeout int) ([]byte, error) {
|
||||||
|
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = defaultHTTPTimeout
|
||||||
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: time.Second * 3,
|
Timeout: time.Second * time.Duration(timeout),
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := client.Get(url)
|
response, err := client.Get(url)
|
||||||
|
|
|
@ -16,7 +16,7 @@ type motdResponse struct {
|
||||||
|
|
||||||
func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
motd, err := client.Get(portainer.MessageOfTheDayURL)
|
motd, err := client.Get(portainer.MessageOfTheDayURL, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -26,7 +26,7 @@ func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *ht
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var templateData []byte
|
var templateData []byte
|
||||||
templateData, err = client.Get(settings.TemplatesURL)
|
templateData, err = client.Get(settings.TemplatesURL, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,13 @@ type Service struct{}
|
||||||
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
||||||
var userDN string
|
var userDN string
|
||||||
found := false
|
found := false
|
||||||
|
usernameEscaped := ldap.EscapeFilter(username)
|
||||||
|
|
||||||
for _, searchSettings := range settings {
|
for _, searchSettings := range settings {
|
||||||
searchRequest := ldap.NewSearchRequest(
|
searchRequest := ldap.NewSearchRequest(
|
||||||
searchSettings.BaseDN,
|
searchSettings.BaseDN,
|
||||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, username),
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
|
||||||
[]string{"dn"},
|
[]string{"dn"},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
@ -134,12 +136,13 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
|
||||||
// Get a list of group names for specified user from LDAP/AD
|
// Get a list of group names for specified user from LDAP/AD
|
||||||
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
|
||||||
groups := make([]string, 0)
|
groups := make([]string, 0)
|
||||||
|
userDNEscaped := ldap.EscapeFilter(userDN)
|
||||||
|
|
||||||
for _, searchSettings := range settings {
|
for _, searchSettings := range settings {
|
||||||
searchRequest := ldap.NewSearchRequest(
|
searchRequest := ldap.NewSearchRequest(
|
||||||
searchSettings.GroupBaseDN,
|
searchSettings.GroupBaseDN,
|
||||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDN),
|
fmt.Sprintf("(&%s(%s=%s))", searchSettings.GroupFilter, searchSettings.GroupAttribute, userDNEscaped),
|
||||||
[]string{"cn"},
|
[]string{"cn"},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
|
@ -639,7 +639,7 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.19.2-dev"
|
APIVersion = "1.20-dev"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 14
|
DBVersion = 14
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
|
|
@ -54,7 +54,7 @@ info:
|
||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.19.2-dev"
|
version: "1.20-dev"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -2816,7 +2816,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.19.2-dev"
|
example: "1.20-dev"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -1,36 +1,26 @@
|
||||||
angular
|
angular
|
||||||
.module('portainer.docker')
|
.module('portainer.docker')
|
||||||
.controller('ContainerRestartPolicyController', [
|
.controller('ContainerRestartPolicyController', [function ContainerRestartPolicyController() {
|
||||||
function ContainerRestartPolicyController() {
|
var ctrl = this;
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
editMode :false,
|
editModel : {}
|
||||||
editModel :{}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
|
ctrl.save = save;
|
||||||
|
|
||||||
ctrl.toggleEdit = toggleEdit;
|
function save() {
|
||||||
ctrl.save = save;
|
if (ctrl.state.editModel.name === ctrl.name && ctrl.state.editModel.maximumRetryCount === ctrl.maximumRetryCount) {
|
||||||
|
return;
|
||||||
function toggleEdit() {
|
|
||||||
ctrl.state.editMode = true;
|
|
||||||
ctrl.state.editModel = {
|
|
||||||
name: ctrl.name,
|
|
||||||
maximumRetryCount: ctrl.maximumRetryCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function save() {
|
|
||||||
if (ctrl.state.editModel.name === ctrl.name &&
|
|
||||||
ctrl.state.editModel.maximumRetryCount === ctrl.maximumRetryCount) {
|
|
||||||
ctrl.state.editMode = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctrl.updateRestartPolicy(ctrl.state.editModel)
|
|
||||||
.then(function onUpdateSucceed() {
|
|
||||||
ctrl.state.editMode = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
ctrl.updateRestartPolicy(ctrl.state.editModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$onInit = function() {
|
||||||
|
ctrl.state.editModel = {
|
||||||
|
name: ctrl.name ? ctrl.name : 'no',
|
||||||
|
maximumRetryCount: ctrl.maximumRetryCount
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
|
@ -1,23 +1,5 @@
|
||||||
<div>
|
<div>
|
||||||
<table class="table table-bordered table-condensed" ng-if="!$ctrl.state.editMode">
|
<table class="table table-bordered table-condensed">
|
||||||
<tr>
|
|
||||||
<td class="col-md-3">
|
|
||||||
<a href="" data-toggle="tooltip" title="Edit restart policy" ng-click="$ctrl.toggleEdit()">
|
|
||||||
<i class="fa fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<span>Name</span>
|
|
||||||
</td>
|
|
||||||
<td>{{$ctrl.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr ng-if="$ctrl.name === 'on-failure'">
|
|
||||||
<td class="col-md-3">Maximum Retry Count</td>
|
|
||||||
<td>
|
|
||||||
{{ $ctrl.maximumRetryCount }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<table class="table table-bordered table-condensed" ng-if="$ctrl.state.editMode">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="col-md-3">
|
<td class="col-md-3">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
|
@ -31,15 +13,14 @@
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-md-2">
|
<td class="col-md-2">
|
||||||
<button class="btn btn-success" ng-click="$ctrl.save()">Save</button>
|
<button class="btn btn-sm btn-primary" ng-click="$ctrl.save()">Update</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.state.editModel.name === 'on-failure'">
|
<tr ng-if="$ctrl.state.editModel.name === 'on-failure'">
|
||||||
<td class="col-md-3">Maximum Retry Count</td>
|
<td class="col-md-3">Maximum Retry Count</td>
|
||||||
<td>
|
<td colspan="2">
|
||||||
<input type="number" class="form-control" ng-model="$ctrl.state.editModel.maximumRetryCount" />
|
<input type="number" class="form-control" ng-model="$ctrl.state.editModel.maximumRetryCount" />
|
||||||
</td>
|
</td>
|
||||||
<td class="col-md-2"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
|
@ -65,7 +65,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." >
|
<div class="btn-group btn-group-xs" role="group" aria-label="..." >
|
||||||
<a style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({ id: item.Container.Id, nodeName: item.Container.NodeName })" title="Logs" ng-if="$ctrl.agentProxy"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
<a style="margin: 0 2.5px;" ui-sref="docker.containers.container.logs({ id: item.Container.Id, nodeName: item.Container.NodeName })" title="Logs" ng-if="$ctrl.agentProxy && item.Container.Id"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
||||||
<a style="margin: 0 2.5px;" ui-sref="docker.tasks.task.logs({ id: item.Id })" title="Logs" ng-if="!$ctrl.agentProxy && $ctrl.showTaskLogsButton && item.Status.State|taskhaslogs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
<a style="margin: 0 2.5px;" ui-sref="docker.tasks.task.logs({ id: item.Id })" title="Logs" ng-if="!$ctrl.agentProxy && $ctrl.showTaskLogsButton && item.Status.State|taskhaslogs"><i class="fa fa-file-alt space-right" aria-hidden="true"></i></a>
|
||||||
<a style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({ id: item.Container.Id, nodeName: item.Container.NodeName })" title="Console" ng-if="$ctrl.agentProxy && item.Status.State === 'running'"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
<a style="margin: 0 2.5px;" ui-sref="docker.containers.container.console({ id: item.Container.Id, nodeName: item.Container.NodeName })" title="Console" ng-if="$ctrl.agentProxy && item.Status.State === 'running'"><i class="fa fa-terminal space-right" aria-hidden="true"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -226,7 +226,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>Restart policies</td>
|
<td>Restart policies</td>
|
||||||
<td>
|
<td>
|
||||||
<container-restart-policy
|
<container-restart-policy ng-if="container"
|
||||||
name="container.HostConfig.RestartPolicy.Name"
|
name="container.HostConfig.RestartPolicy.Name"
|
||||||
maximum-retry-count="container.HostConfig.RestartPolicy.MaximumRetryCount"
|
maximum-retry-count="container.HostConfig.RestartPolicy.MaximumRetryCount"
|
||||||
update-restart-policy="updateRestartPolicy(name, maximumRetryCount)">
|
update-restart-policy="updateRestartPolicy(name, maximumRetryCount)">
|
||||||
|
|
|
@ -323,6 +323,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
||||||
Name: restartPolicy,
|
Name: restartPolicy,
|
||||||
MaximumRetryCount: maximumRetryCount
|
MaximumRetryCount: maximumRetryCount
|
||||||
};
|
};
|
||||||
|
Notifications.success('Restart policy updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
function notifyOnError(err) {
|
function notifyOnError(err) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
|
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
|
||||||
function ($q, $scope, $state, $filter, EndpointService, GroupService, TagService, Notifications) {
|
function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, TagService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
EnvironmentType: 'docker',
|
EnvironmentType: 'docker',
|
||||||
|
@ -19,6 +19,12 @@ function ($q, $scope, $state, $filter, EndpointService, GroupService, TagService
|
||||||
Tags: []
|
Tags: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.copyAgentCommand = function() {
|
||||||
|
clipboard.copyText('curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
|
||||||
|
$('#copyNotification').show();
|
||||||
|
$('#copyNotification').fadeOut(2000);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addDockerEndpoint = function() {
|
$scope.addDockerEndpoint = function() {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||||
|
|
|
@ -64,8 +64,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
If you have started Portainer in the same overlay network as the agent, you can use <code>tasks.AGENT_SERVICE_NAME:AGENT_SERVICE_PORT</code> as the
|
Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it.
|
||||||
endpoint URL format.
|
<div style="margin-top: 10px;">
|
||||||
|
<code>
|
||||||
|
curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
|
||||||
|
</code>
|
||||||
|
<span class="btn btn-primary btn-sm space-left" ng-click="copyAgentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||||
|
<span>
|
||||||
|
<i id="copyNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
2
build.sh
2
build.sh
|
@ -24,7 +24,7 @@ function build_archive() {
|
||||||
function build_all() {
|
function build_all() {
|
||||||
mkdir -pv "${ARCHIVE_BUILD_FOLDER}"
|
mkdir -pv "${ARCHIVE_BUILD_FOLDER}"
|
||||||
for tag in $@; do
|
for tag in $@; do
|
||||||
grunt "release:`echo "$tag" | tr '-' ':'`"
|
yarn grunt "release:`echo "$tag" | tr '-' ':'`"
|
||||||
name="portainer"; if [ "$(echo "$tag" | cut -c1)" = "w" ]; then name="${name}.exe"; fi
|
name="portainer"; if [ "$(echo "$tag" | cut -c1)" = "w" ]; then name="${name}.exe"; fi
|
||||||
mv dist/portainer-$tag* dist/$name
|
mv dist/portainer-$tag* dist/$name
|
||||||
if [ `echo $tag | cut -d \- -f 1` == 'linux' ]; then build_and_push_images "$tag"; fi
|
if [ `echo $tag | cut -d \- -f 1` == 'linux' ]; then build_and_push_images "$tag"; fi
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Name: portainer
|
Name: portainer
|
||||||
Version: 1.19.2-dev
|
Version: 1.20-dev
|
||||||
Release: 0
|
Release: 0
|
||||||
License: Zlib
|
License: Zlib
|
||||||
Summary: A lightweight docker management UI
|
Summary: A lightweight docker management UI
|
||||||
|
|
|
@ -80,7 +80,7 @@ module.exports = function (grunt) {
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
root: 'dist',
|
root: 'dist',
|
||||||
distdir: 'dist/public',
|
distdir: 'dist/public',
|
||||||
shippedDockerVersion: '18.03.1-ce',
|
shippedDockerVersion: '18.06.1-ce',
|
||||||
shippedDockerVersionWindows: '17.09.0-ce',
|
shippedDockerVersionWindows: '17.09.0-ce',
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
pkg: grunt.file.readJSON('package.json'),
|
||||||
config: gruntfile_cfg.config,
|
config: gruntfile_cfg.config,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "1.19.2-dev",
|
"version": "1.20-dev",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
@ -20,6 +20,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"grunt": "grunt",
|
||||||
"dev": "yarn grunt run-dev",
|
"dev": "yarn grunt run-dev",
|
||||||
"clean-all": "yarn grunt clean:all",
|
"clean-all": "yarn grunt clean:all",
|
||||||
"build": "yarn grunt build",
|
"build": "yarn grunt build",
|
||||||
|
|
Loading…
Reference in New Issue