mirror of https://github.com/portainer/portainer
feat(docker-desktop-extension): Make Portainer compatible with Docker Desktop Extension EE-2747 (#6644)
* Initial extension build * Add auto login fix auto auth add some message Add extension version Double attempt to login Add auto login from jwt check Add autologin on logout revert sidebar Catch error 401 to relogin cleanup login Add password generator Hide User block and collapse sidebar by default hide user box and toggle sidebar remove defailt dd Integrate extension to portainer Move extension to build remove files from ignore Move extension folder fix alpine try to copy folder try add Change base image move folder extension ignore folder build Fix relative path Move ext to root fix image name versioned index Update extension on same image Update mod * fix kubeshell baseurl * Fix kube shell * move build and remove https * Tidy mod * Remove space * Fix hash test * Password manager * change to building locally * Restore version variable and add local install command * fix local dev image + hide users & auth * Password manageListen on locahost onlyr * FIxes base path * Hide only username * Move default to constants * Update app/portainer/components/PageHeader/HeaderContent.html Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com> * fix 2 failing FE tests [EE-2938] * remove password autogeneration from v1 * fix webhooks * fix docker container console and attach * fix default for portainer IP * update meta, dockerfile and makefile for new ver * fix basepath in kube and docker console * revert makefile changes * add icon back * Add remote short cut command * make local methods the default * default to 0.0.0 for version for local development * simplify make commands * small build fixes * resolve conflicts * Update api/filesystem/write.go Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com> * use a more secure default pass Co-authored-by: itsconquest <william.conquest@portainer.io> Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>pull/6775/head
parent
7efdae5eee
commit
360701e256
|
@ -1,3 +1,5 @@
|
|||
*
|
||||
!dist
|
||||
!build
|
||||
!metadata.json
|
||||
!docker-extension/build
|
||||
|
|
|
@ -51,7 +51,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
|
||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
|
|
|
@ -9,11 +9,11 @@ type Service struct{}
|
|||
|
||||
// Hash hashes a string using the bcrypt algorithm
|
||||
func (*Service) Hash(data string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService_Hash(t *testing.T) {
|
||||
var s = &Service{}
|
||||
|
||||
type args struct {
|
||||
hash string
|
||||
data string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
args: args{
|
||||
hash: "",
|
||||
data: "",
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "Matching",
|
||||
args: args{
|
||||
hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy",
|
||||
data: "Passw0rd!",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "Not matching",
|
||||
args: args{
|
||||
hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a",
|
||||
data: "Passw0rd!",
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
err := s.CompareHashAndData(tt.args.hash, tt.args.data)
|
||||
if (err != nil) == tt.expect {
|
||||
t.Errorf("Service.CompareHashAndData() = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WriteToFile creates a file in the filesystem storage
|
||||
func WriteToFile(dst string, content []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
|
||||
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
|
||||
|
|
|
@ -1344,7 +1344,7 @@ type (
|
|||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.11.0"
|
||||
APIVersion = "2.11.1"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 35
|
||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||
|
|
|
@ -14,6 +14,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
|
|||
tokenGetter: /* @ngInject */ function tokenGetter(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
},
|
||||
whiteListedDomains: ['localhost'],
|
||||
});
|
||||
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
|
|
|
@ -69,9 +69,9 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
|||
id: attachId,
|
||||
};
|
||||
|
||||
const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref();
|
||||
var url =
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
base +
|
||||
'api/websocket/attach?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
@ -110,9 +110,9 @@ angular.module('portainer.docker').controller('ContainerConsoleController', [
|
|||
id: data.Id,
|
||||
};
|
||||
|
||||
const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref();
|
||||
var url =
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
base +
|
||||
'api/websocket/exec?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
|
|
@ -18,3 +18,7 @@ declare module 'axios-progress-bar' {
|
|||
instance?: AxiosInstance
|
||||
): void;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
ddExtension: boolean;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,16 @@
|
|||
<meta name="author" content="<%= author %>" />
|
||||
<base id="base" />
|
||||
<script>
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
if (window.origin == 'file://') {
|
||||
// we are loading the app from a local file as in docker extension
|
||||
document.getElementById('base').href = 'http://localhost:9000/';
|
||||
|
||||
window.ddExtension = true;
|
||||
} else {
|
||||
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
|
||||
var basePath = path ? '/' + path + '/' : '/';
|
||||
document.getElementById('base').href = basePath;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
|
@ -62,9 +69,12 @@
|
|||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div> </div
|
||||
><!-- End Page Content --> </div
|
||||
><!-- End Content Wrapper --> </div
|
||||
><!-- End Page Wrapper -->
|
||||
</body></html
|
||||
>
|
||||
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div>
|
||||
</div>
|
||||
<!-- End Page Content -->
|
||||
</div>
|
||||
<!-- End Content Wrapper -->
|
||||
</div>
|
||||
<!-- End Page Wrapper -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -93,11 +93,13 @@ export default class KubectlShellController {
|
|||
|
||||
const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
const path = baseHref() + 'api/websocket/kubernetes-shell';
|
||||
const base = path.startsWith('http') ? path.replace(/^https?:\/\//i, '') : window.location.host + path;
|
||||
|
||||
const queryParams = Object.entries(params)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('&');
|
||||
const url = `${wsProtocol}${window.location.host}${path}?${queryParams}`;
|
||||
|
||||
const url = `${wsProtocol}${base}?${queryParams}`;
|
||||
Terminal.applyAddon(fit);
|
||||
this.state.shell.socket = new WebSocket(url);
|
||||
this.state.shell.term = new Terminal();
|
||||
|
|
|
@ -58,9 +58,10 @@ class KubernetesApplicationConsoleController {
|
|||
command: this.state.command,
|
||||
};
|
||||
|
||||
const base = window.location.origin.startsWith('http') ? `${window.location.origin}${baseHref()}` : baseHref();
|
||||
|
||||
let url =
|
||||
window.location.origin +
|
||||
baseHref() +
|
||||
base +
|
||||
'api/websocket/pod?' +
|
||||
Object.keys(params)
|
||||
.map((k) => k + '=' + params[k])
|
||||
|
|
|
@ -2,7 +2,7 @@ export default class HeaderContentController {
|
|||
/* @ngInject */
|
||||
constructor(Authentication) {
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.display = !window.ddExtension;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class="breadcrumb-links">
|
||||
<div class="pull-left" ng-transclude></div>
|
||||
<div class="pull-right" ng-if="$ctrl.username">
|
||||
<div class="pull-right" ng-if="$ctrl.username && $ctrl.display">
|
||||
<a ui-sref="portainer.account" style="margin-right: 5px">
|
||||
<u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
|
||||
<u> <i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
|
||||
</a>
|
||||
<a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px" data-cy="template-logoutButton">
|
||||
<u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
|
||||
<u> <i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ export function HeaderContent({ children }: PropsWithChildren<unknown>) {
|
|||
return (
|
||||
<div className="breadcrumb-links">
|
||||
<div className="pull-left">{children}</div>
|
||||
{user && (
|
||||
{user && !window.ddExtension && (
|
||||
<div className={clsx('pull-right', styles.userLinks)}>
|
||||
<Link to="portainer.account" className={styles.link}>
|
||||
<i
|
||||
|
|
|
@ -2,7 +2,7 @@ export default class HeaderTitle {
|
|||
/* @ngInject */
|
||||
constructor(Authentication) {
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.display = !window.ddExtension;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<div class="page white-space-normal">
|
||||
{{ $ctrl.titleText }}
|
||||
<span class="header_title_content" ng-transclude></span>
|
||||
<span class="pull-right user-box" ng-if="$ctrl.username"> <i class="fa fa-user-circle" aria-hidden="true"></i> {{ $ctrl.username }} </span>
|
||||
<span class="pull-right user-box" ng-if="$ctrl.username && $ctrl.display">
|
||||
<i class="fa fa-user-circle" aria-hidden="true"></i>
|
||||
{{ $ctrl.username }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -17,9 +17,10 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
|
|||
<div className="page white-space-normal">
|
||||
{title}
|
||||
<span className="header_title_content">{children}</span>
|
||||
{user && (
|
||||
{user && !window.ddExtension && (
|
||||
<span className="pull-right user-box">
|
||||
<i className="fa fa-user-circle" aria-hidden="true" /> {user.Username}
|
||||
<i className="fa fa-user-circle" aria-hidden="true" />
|
||||
{user.Username}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -8,16 +8,24 @@ angular.module('portainer.app').factory('WebhookHelper', [
|
|||
'use strict';
|
||||
|
||||
var helper = {};
|
||||
let base;
|
||||
const protocol = $location.protocol().toLowerCase();
|
||||
const port = $location.port();
|
||||
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
|
||||
|
||||
if (protocol !== 'file') {
|
||||
const host = $location.host();
|
||||
const port = $location.port();
|
||||
const displayPort = (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443) ? '' : ':' + port;
|
||||
base = `${protocol}://${host}${displayPort}${baseHref()}`;
|
||||
} else {
|
||||
base = baseHref();
|
||||
}
|
||||
|
||||
helper.returnWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}${baseHref()}${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
return `${base}${API_ENDPOINT_WEBHOOKS}/${token}`;
|
||||
};
|
||||
|
||||
helper.returnStackWebhookUrl = function (token) {
|
||||
return `${protocol}://${$location.host()}${displayPort}${baseHref()}${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
return `${base}${API_ENDPOINT_STACKS}/webhooks/${token}`;
|
||||
};
|
||||
|
||||
return helper;
|
||||
|
|
|
@ -31,9 +31,11 @@ angular.module('portainer.app').factory('EndpointStatusInterceptor', [
|
|||
}
|
||||
|
||||
function responseErrorInterceptor(rejection) {
|
||||
var url = rejection.config.url;
|
||||
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
EndpointProvider.setOfflineMode(true);
|
||||
if (rejection.config) {
|
||||
var url = rejection.config.url;
|
||||
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
|
||||
EndpointProvider.setOfflineMode(true);
|
||||
}
|
||||
}
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { clear as clearSessionStorage } from './session-storage';
|
||||
|
||||
const DEFAULT_USER = 'admin';
|
||||
const DEFAULT_PASSWORD = 'K7yJPP5qNK4hf1QsRnfV';
|
||||
|
||||
angular.module('portainer.app').factory('Authentication', [
|
||||
'$async',
|
||||
'$state',
|
||||
|
@ -28,12 +31,14 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
async function initAsync() {
|
||||
try {
|
||||
const jwt = LocalStorage.getJWT();
|
||||
if (jwt) {
|
||||
await setUser(jwt);
|
||||
if (!jwt || jwtHelper.isTokenExpired(jwt)) {
|
||||
return tryAutoLoginExtension();
|
||||
}
|
||||
return !!jwt;
|
||||
await setUser(jwt);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
console.log('Unable to initialize authentication service', error);
|
||||
return tryAutoLoginExtension();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,6 +52,7 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
EndpointProvider.clean();
|
||||
LocalStorage.cleanAuthData();
|
||||
LocalStorage.storeLoginStateUUID('');
|
||||
tryAutoLoginExtension();
|
||||
}
|
||||
|
||||
function logout(performApiLogout) {
|
||||
|
@ -59,7 +65,15 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
|
||||
async function OAuthLoginAsync(code) {
|
||||
const response = await OAuth.validate({ code: code }).$promise;
|
||||
await setUser(response.jwt);
|
||||
const jwt = setJWTFromResponse(response);
|
||||
await setUser(jwt);
|
||||
}
|
||||
|
||||
function setJWTFromResponse(response) {
|
||||
const jwt = response.jwt;
|
||||
LocalStorage.storeJWT(jwt);
|
||||
|
||||
return response.jwt;
|
||||
}
|
||||
|
||||
function OAuthLogin(code) {
|
||||
|
@ -68,7 +82,8 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
|
||||
async function loginAsync(username, password) {
|
||||
const response = await Auth.login({ username: username, password: password }).$promise;
|
||||
await setUser(response.jwt);
|
||||
const jwt = setJWTFromResponse(response);
|
||||
await setUser(jwt);
|
||||
}
|
||||
|
||||
function login(username, password) {
|
||||
|
@ -77,7 +92,7 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
|
||||
function isAuthenticated() {
|
||||
var jwt = LocalStorage.getJWT();
|
||||
return jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
return !!jwt && !jwtHelper.isTokenExpired(jwt);
|
||||
}
|
||||
|
||||
function getUserDetails() {
|
||||
|
@ -96,7 +111,6 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
}
|
||||
|
||||
async function setUser(jwt) {
|
||||
LocalStorage.storeJWT(jwt);
|
||||
var tokenPayload = jwtHelper.decodeToken(jwt);
|
||||
user.username = tokenPayload.username;
|
||||
user.ID = tokenPayload.id;
|
||||
|
@ -105,11 +119,16 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
await setUserTheme();
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
if (user.role === 1) {
|
||||
return true;
|
||||
function tryAutoLoginExtension() {
|
||||
if (!window.ddExtension) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
return login(DEFAULT_USER, DEFAULT_PASSWORD);
|
||||
}
|
||||
|
||||
function isAdmin() {
|
||||
return !!user && user.role === 1;
|
||||
}
|
||||
|
||||
return service;
|
||||
|
|
|
@ -90,8 +90,16 @@ angular
|
|||
};
|
||||
|
||||
$scope.setDefaultPortainerInstanceURL = function () {
|
||||
const baseHREF = baseHref();
|
||||
$scope.formValues.URL = window.location.origin + (baseHREF !== '/' ? baseHREF : '');
|
||||
let url;
|
||||
|
||||
if (window.location.origin.startsWith('http')) {
|
||||
const path = baseHref() !== '/' ? path : '';
|
||||
url = `${window.location.origin}${path}`;
|
||||
} else {
|
||||
url = baseHref().replace(/\/$/, '');
|
||||
}
|
||||
|
||||
$scope.formValues.URL = url;
|
||||
};
|
||||
|
||||
$scope.resetEndpointURL = function () {
|
||||
|
|
|
@ -19,7 +19,7 @@ angular.module('portainer.app').controller('MainController', [
|
|||
$scope.$watch($scope.getWidth, function (newValue) {
|
||||
if (newValue >= mobileView) {
|
||||
const toggleValue = LocalStorage.getToolbarToggle();
|
||||
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true;
|
||||
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : !window.ddExtension;
|
||||
} else {
|
||||
$scope.toggle = false;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_logo" class="control-label text-left"> Use custom logo </label>
|
||||
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px">
|
||||
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="formValues.customLogo">
|
||||
|
@ -40,7 +43,10 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_enableTelemetry" class="control-label text-left"> Allow the collection of anonymous statistics </label>
|
||||
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" /><i></i> </label>
|
||||
<label class="switch" style="margin-left: 20px">
|
||||
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
|
||||
You can find more information about this in our
|
||||
|
@ -128,7 +134,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ssl-certificate-settings></ssl-certificate-settings>
|
||||
<ssl-certificate-settings ng-show="$ctrl.showHTTPS"></ssl-certificate-settings>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
|
@ -149,7 +155,7 @@
|
|||
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar" />
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-2 margin-sm-top">
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -166,11 +172,11 @@
|
|||
<tr ng-repeat="label in settings.BlackListedLabels">
|
||||
<td>{{ label.name }}</td>
|
||||
<td>{{ label.value }}</td>
|
||||
<td
|
||||
><button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)"
|
||||
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
<button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="settings.BlackListedLabels.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No filter available.</td>
|
||||
|
@ -208,8 +214,8 @@
|
|||
checked="formValues.scheduleAutomaticBackups"
|
||||
label-class="'col-sm-2'"
|
||||
on-change="(onToggleAutoBackups)"
|
||||
></por-switch-field
|
||||
></div>
|
||||
></por-switch-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Schedule automatic backups -->
|
||||
<!-- Cron rule -->
|
||||
|
@ -311,7 +317,8 @@
|
|||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch" data-cy="settings-s3PasswordToggle">
|
||||
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled /><i></i>
|
||||
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -342,7 +349,7 @@
|
|||
limited-feature-disabled
|
||||
limited-feature-class="limited-be"
|
||||
>
|
||||
<span><i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
|
||||
<span> <i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -370,7 +377,8 @@
|
|||
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
|
||||
<div class="col-sm-1">
|
||||
<label class="switch" data-cy="settings-passwordProtectLocal">
|
||||
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i>
|
||||
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" />
|
||||
<i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,6 +45,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
],
|
||||
backupInProgress: false,
|
||||
featureLimited: false,
|
||||
showHTTPS: !window.ddExtension,
|
||||
};
|
||||
|
||||
$scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
|
||||
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
|
||||
<sidebar-menu
|
||||
ng-show="display"
|
||||
icon-class="fa-users fa-fw"
|
||||
label="Users"
|
||||
path="portainer.users"
|
||||
|
@ -95,7 +96,12 @@
|
|||
is-sidebar-open="toggle"
|
||||
children-paths="['portainer.settings.authentication', 'portainer.settings.edgeCompute']"
|
||||
>
|
||||
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication" title="Authentication"
|
||||
<sidebar-menu-item
|
||||
path="portainer.settings.authentication"
|
||||
class-name="sidebar-sublist"
|
||||
data-cy="portainerSidebar-authentication"
|
||||
title="Authentication"
|
||||
ng-show="display"
|
||||
>Authentication</sidebar-menu-item
|
||||
>
|
||||
<sidebar-menu-item path="portainer.settings.edgeCompute" class-name="sidebar-sublist" data-cy="portainerSidebar-edge-compute" title="Edge Compute"
|
||||
|
|
|
@ -3,6 +3,7 @@ angular.module('portainer.app').controller('SidebarController', SidebarControlle
|
|||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
|
||||
$scope.applicationState = StateManager.getState();
|
||||
$scope.endpointState = EndpointProvider.endpoint();
|
||||
$scope.display = !window.ddExtension;
|
||||
|
||||
function checkPermissions(memberships) {
|
||||
var isLeader = false;
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# Makefile for development purpose
|
||||
|
||||
.PHONY: local build
|
||||
local: clean build install
|
||||
remote: clean build-remote install
|
||||
|
||||
ORG=portainer
|
||||
VERSION=0.0.0
|
||||
IMAGE_NAME=$(ORG)/portainer-docker-extension
|
||||
TAGGED_IMAGE_NAME=$(IMAGE_NAME):$(VERSION)
|
||||
|
||||
clean:
|
||||
-docker extension remove $(IMAGE_NAME)
|
||||
-docker rmi $(IMAGE_NAME):$(VERSION)
|
||||
|
||||
build:
|
||||
docker buildx build -f build/linux/Dockerfile --load --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
|
||||
|
||||
build-remote:
|
||||
docker buildx build -f build/linux/Dockerfile --push --builder=buildx-multi-arch --platform=windows/amd64,linux/amd64,linux/arm64 --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
|
||||
|
||||
install:
|
||||
docker extension install $(TAGGED_IMAGE_NAME)
|
||||
|
||||
multiarch:
|
||||
docker buildx create --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
|
||||
|
||||
portainer:
|
||||
yarn build
|
|
@ -0,0 +1,18 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
portainer:
|
||||
image: ${DESKTOP_PLUGIN_IMAGE}
|
||||
command: ['--admin-password', '$$$$2y$$$$05$$$$bsb.XmF.r2DU6/9oVUaDxu3.Lxhmg1R8M0NMLK6JJKUiqUcaNjvdu']
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- portainer_data:/data
|
||||
ports:
|
||||
- 127.0.0.1:8000:8000
|
||||
- 127.0.0.1:9000:9000
|
||||
- 127.0.0.1:9443:9443
|
||||
volumes:
|
||||
portainer_data:
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "Portainer",
|
||||
"icon": "portainer.svg",
|
||||
"vm": {
|
||||
"composefile": "docker-compose.yml",
|
||||
"exposes": { "socket": "docker.sock" }
|
||||
},
|
||||
"ui": {
|
||||
"dashboard-tab": {
|
||||
"title": "Portainer",
|
||||
"root": "/public",
|
||||
"src": "index.html"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<svg height="2500" viewBox=".16 0 571.71 800" width="1788" xmlns="http://www.w3.org/2000/svg"><g fill="#13bef9"><path d="m190.83 175.88h-12.2v63.2h12.2zm52.47 0h-12.2v63.2h12.2zm71.69-120.61-12.5-21.68-208.67 120.61 12.5 21.68z"/><path d="m313.77 55.27 12.51-21.68 208.67 120.61-12.51 21.68z"/><path d="m571.87 176.18v-25.03h-571.71v25.03z"/><path d="m345.5 529.77v-370.99h25.02v389.01c-6.71-7.64-15.26-13.13-25.02-18.02zm-42.71-6.41v-523.36h25.02v526.41c-7.02-3.36-24.1-3.05-25.02-3.05zm-237.04 52.21c-30.51-22.59-50.64-58.62-50.64-99.54 0-21.68 5.79-43.05 16.47-61.68h213.55c10.98 18.63 16.48 40 16.48 61.68 0 18.93-2.44 36.64-10.07 52.52-16.17-15.57-39.97-22.29-64.07-22.29-42.71 0-79.32 26.56-88.77 66.26-3.36-.31-5.49-.61-8.85-.61-8.24.3-16.17 1.53-24.1 3.66z" fill-rule="evenodd"/><path d="m170.69 267.18h-64.67v65.03h64.67zm-72.91 0h-64.67v65.03h64.67zm0 72.36h-64.67v65.04h64.67zm72.91 0h-64.67v65.04h64.67zm72.61 0h-64.67v65.04h64.67zm0-107.17h-64.67v65.03h64.67z"/><path d="m109.37 585.34c8.85-37.55 42.71-65.65 82.98-65.65 25.94 0 49.12 11.61 64.99 29.93 13.72-9.47 30.2-14.96 48.2-14.96 46.98 0 85.11 38.16 85.11 85.19 0 9.77-1.52 18.93-4.57 27.78 10.37 14.05 16.78 31.76 16.78 50.69 0 47.02-38.14 85.19-85.12 85.19-20.75 0-39.66-7.33-54.3-19.54-15.56 21.68-40.88 36.03-69.56 36.03-32.95 0-61.63-18.93-75.96-46.41-5.8 1.22-11.6 1.83-17.7 1.83-46.98 0-85.42-38.17-85.42-85.19s38.14-85.19 85.42-85.19c3.05-.31 6.1-.31 9.15.3z" fill-rule="evenodd"/></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,6 +1,13 @@
|
|||
FROM portainer/base
|
||||
|
||||
LABEL org.opencontainers.image.title="Portainer" \
|
||||
org.opencontainers.image.description="Rich container management experience using Portainer." \
|
||||
org.opencontainers.image.vendor="Portainer.io" \
|
||||
com.docker.desktop.extension.api.version=">= 0.2.2" \
|
||||
com.docker.desktop.extension.icon=https://portainer-io-assets.sfo2.cdn.digitaloceanspaces.com/logos/portainer.png
|
||||
|
||||
COPY dist /
|
||||
COPY build/docker-extension /
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
|
||||
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
|
||||
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt start:client",
|
||||
"dev:extension": "grunt build && make local -f build/docker-extension/Makefile",
|
||||
"start:toolkit": "grunt start:toolkit",
|
||||
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
|
||||
"clean:all": "grunt clean:all",
|
||||
|
|
Loading…
Reference in New Issue