add operators UI

This commit is contained in:
Girish Ramakrishnan
2021-09-21 15:26:05 -07:00
parent ee62e9c2e7
commit aecba53de5
5 changed files with 126 additions and 68 deletions
+6 -2
View File
@@ -116,7 +116,7 @@
"locationPlaceholder": "Leave empty to use bare domain",
"manualWarning": "Add an A record manually for <b>{{ location }}</b> to this Cloudron's public IP",
"userManagement": "User management",
"userManagementNone": "This app has its own user management.",
"userManagementNone": "This app has its own user management. This setting determines whether this app is visible in the user's dashboard.",
"userManagementMailbox": "All users with a mailbox on this Cloudron have access.",
"userManagementLeaveToApp": "Leave user management to the app",
"userManagementAllUsers": "Allow all users from this Cloudron",
@@ -1227,7 +1227,7 @@
"accessControl": {
"userManagement": {
"title": "User management",
"description": "This app is configured to authenticate with the Cloudron User Directory.",
"description": "This app is configured to authenticate with the Cloudron User Directory. This setting controls who can log in and use the app.",
"descriptionSftp": "This setting also controls SFTP access.",
"dashboardVisibility": "Dashboard visibility",
"sftpAccessControl": "This setting also controls SFTP access.",
@@ -1239,6 +1239,10 @@
"server": "Server",
"port": "Port",
"username": "Username"
},
"operators": {
"title": "Operators",
"description": "Operators can configure and maintain this app."
}
},
"resources": {
+64 -45
View File
@@ -508,10 +508,10 @@
<div class="col-sm-2">
<div class="app-configure-links">
<div ng-click="setView('display')" ng-class="{ 'active': view === 'display' }">{{ 'app.displayTabTitle' | tr }}</div>
<div ng-click="setView('location')" ng-class="{ 'active': view === 'location' }">{{ 'app.locationTabTitle' | tr }}</div>
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access' }">{{ 'app.accessControlTabTitle' | tr }}</div>
<div ng-click="setView('location')" ng-class="{ 'active': view === 'location' }" ng-show="app.accessLevel === 'admin'">{{ 'app.locationTabTitle' | tr }}</div>
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access' }" ng-show="app.accessLevel === 'admin'">{{ 'app.accessControlTabTitle' | tr }}</div>
<div ng-click="setView('resources')" ng-class="{ 'active': view === 'resources' }">{{ 'app.resourcesTabTitle' | tr }}</div>
<div ng-click="setView('storage')" ng-class="{ 'active': view === 'storage' }">{{ 'app.storageTabTitle' | tr }}</div>
<div ng-click="setView('storage')" ng-class="{ 'active': view === 'storage' }" ng-show="app.accessLevel === 'admin'">{{ 'app.storageTabTitle' | tr }}</div>
<div ng-click="setView('graphs')" ng-class="{ 'active': view === 'graphs' }">{{ 'app.graphsTabTitle' | tr }}</div>
<div ng-click="setView('security')" ng-class="{ 'active': view === 'security' }">{{ 'app.securityTabTitle' | tr }}</div>
<div ng-click="setView('email')" ng-class="{ 'active': view === 'email' }" ng-show="app.manifest.addons.sendmail || app.manifest.addons.recvmail">{{ 'app.emailTabTitle' | tr }}</div>
@@ -519,7 +519,7 @@
<div ng-click="setView('updates')" ng-class="{ 'active': view === 'updates' }">{{ 'app.updatesTabTitle' | tr }}</div>
<div ng-click="setView('backups')" ng-class="{ 'active': view === 'backups' }">{{ 'app.backupsTabTitle' | tr }}</div>
<div ng-click="setView('repair')" ng-class="{ 'active': view === 'repair' }">{{ 'app.repairTabTitle' | tr }}</div>
<div ng-click="setView('uninstall')" ng-class="{ 'active': view === 'uninstall' }">{{ 'app.uninstallTabTitle' | tr }}</div>
<div ng-click="setView('uninstall')" ng-class="{ 'active': view === 'uninstall' }" ng-show="app.accessLevel === 'admin'">{{ 'app.uninstallTabTitle' | tr }}</div>
</div>
</div>
<div class="col-sm-8 card-container">
@@ -682,58 +682,77 @@
<div class="row">
<div class="col-md-12">
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
<div class="form-group">
<div class="form-group" ng-show="app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }}</label>
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
</p>
</div>
<div class="form-group" ng-show="app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }}</label>
<p>{{ 'appstore.installDialog.userManagementMailbox' | tr }}
<span ng-bind-html="'appstore.installDialog.configuredForCloudronEmail' | tr:{ emailDocsLink: 'https://docs.cloudron.io/email/' }">
</p>
</div>
<div ng-show="access.ssoAuth && !app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p>{{ 'app.accessControl.userManagement.description' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.descriptionSftp' | tr }}</span></p>
</div>
<div ng-show="!access.ssoAuth || app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p ng-show="!app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.sftpAccessControl' | tr }}</span></p>
</div>
<div ng-show="access.ssoAuth && !app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#access-restriction" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p>{{ 'app.accessControl.userManagement.description' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.descriptionSftp' | tr }}</span></p>
</div>
<div ng-show="!access.ssoAuth || app.manifest.addons.email">
<label class="control-label">{{ 'app.accessControl.userManagement.dashboardVisibility' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#dashboard-visibility" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p ng-show="!app.manifest.addons.email">{{ 'appstore.installDialog.userManagementNone' | tr }} <span ng-show="access.ftp">{{ 'app.accessControl.userManagement.sftpAccessControl' | tr }}</span></p>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="any">
<span ng-show="access.ssoAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
<span ng-show="!access.ssoAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="groups">
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="any">
<span ng-show="access.ssoAuth">{{ 'appstore.installDialog.userManagementAllUsers' | tr }}</span>
<span ng-show="!access.ssoAuth">{{ 'app.accessControl.userManagement.visibleForAllUsers' | tr }}</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="groups">
<span ng-show="access.ssoAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
<span ng-show="!access.ssoAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
<span ng-show="access.ssoAuth">{{ 'appstore.installDialog.userManagementSelectUsers' | tr }}</span>
<span ng-show="!access.ssoAuth">{{ 'app.accessControl.userManagement.visibleForSelected' | tr }}</span>
<span class="label label-danger" ng-show="access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
{{ 'appstore.installDialog.users' | tr }}: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.users" ng-disabled="access.accessRestrictionOption !== 'groups'" options="user.display for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
<span class="label label-danger" ng-show="access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()">{{ 'appstore.installDialog.errorUserManagementSelectAtLeastOne' | tr }}</span>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
{{ 'appstore.installDialog.users' | tr }}: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.users" ng-disabled="access.accessRestrictionOption !== 'groups'" options="user.display for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
<div class="col-md-5">
{{ 'appstore.installDialog.groups' | tr }}: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.groups" ng-disabled="access.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
<div class="col-md-5">
{{ 'appstore.installDialog.groups' | tr }}: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.groups" ng-disabled="access.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || accessForm.$invalid || access.busy"/>
</div>
<br/>
<br/>
<br/>
<div>
<label class="control-label">{{ 'app.accessControl.operators.title' | tr }} <sup><a ng-href="https://docs.cloudron.io/apps/#operators" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p>{{ 'app.accessControl.operators.description' | tr }}</p>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
{{ 'appstore.installDialog.users' | tr }}: <multiselect class="input-sm stretch" ng-model="access.operators.users" options="user.display for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
<div class="col-md-5">
{{ 'appstore.installDialog.groups' | tr }}: <multiselect class="input-sm stretch" ng-model="access.operators.groups" options="group.name for group in groups" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || accessForm.$invalid || access.busy"/>
</form>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="access.submit()" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || access.$invalid || access.busy"><i class="fa fa-circle-notch fa-spin" ng-show="access.busy"></i> {{ 'main.dialog.save' | tr }}</button>
+48 -17
View File
@@ -11,8 +11,6 @@
/* global SECRET_PLACEHOLDER */
angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) {
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
@@ -452,6 +450,8 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
operators: { users: [], groups: [] },
isAccessRestrictionValid: function () {
var tmp = $scope.access.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
@@ -466,15 +466,29 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
$scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
$scope.access.accessRestriction = { users: [], groups: [] };
$scope.access.operators = { users: [], groups: [] };
var userSet, groupSet;
if (app.accessRestriction) {
var userSet = { };
userSet = {};
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); });
var groupSet = { };
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
groupSet = {};
if (app.accessRestriction.groups) app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); });
}
if (app.operators) {
userSet = {};
app.operators.users.forEach(function (uid) { userSet[uid] = true; });
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.operators.users.push(u); });
groupSet = {};
if (app.operators.groups) app.operators.groups.forEach(function (gid) { groupSet[gid] = true; });
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.operators.groups.push(g); });
}
},
submit: function () {
@@ -488,13 +502,24 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; });
}
var operators = null;
if ($scope.access.operators.users.length || $scope.access.operators.groups.length) {
operators = { users: [], groups: [] };
operators.users = $scope.access.operators.users.map(function (u) { return u.id; });
operators.groups = $scope.access.operators.groups.map(function (g) { return g.id; });
}
Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, function (error) {
if (error) return Client.error(error);
$timeout(function () {
$scope.access.success = true;
$scope.access.busy = false;
}, 1000);
Client.configureApp($scope.app.id, 'operators', { operators: operators }, function (error) {
if (error) return Client.error(error);
$timeout(function () {
$scope.access.success = true;
$scope.access.busy = false;
}, 1000);
});
});
}
};
@@ -1681,8 +1706,6 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
return;
}
console.log(backupConfig)
$scope.$apply(function () {
// we assume property names match here, this does not yet work for gcs keys
Object.keys(backupConfig).forEach(function (k) {
@@ -1699,6 +1722,8 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
refreshApp(appId, function (error) {
if (error) return Client.error(error);
if ($scope.app.accessLevel !== 'admin' && $scope.app.accessLevel !== 'operator') return $location.path('/');
// skipViewShow because we don't have all the values like domains/users to init the view yet
if ($routeParams.view) { // explicit route in url bar
$scope.setView($routeParams.view, true /* skipViewShow */);
@@ -1706,6 +1731,17 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
$scope.setView($scope.app.error ? 'repair' : 'display', true /* skipViewShow */);
}
function done() {
$scope[$scope.view].show(); // initialize now that we have all the values
var refreshTimer = $interval(function () { refreshApp($scope.app.id); }, 5000); // call with inline function to avoid iteration argument passed see $interval docs
$scope.$on('$destroy', function () {
$interval.cancel(refreshTimer);
});
}
if ($scope.app.accessLevel !== 'admin') return done();
async.series([
fetchUsers,
fetchGroups,
@@ -1715,12 +1751,7 @@ angular.module('Application').controller('AppController', ['$scope', '$location'
], function (error) {
if (error) return Client.error(error);
$scope[$scope.view].show(); // initialize now that we have all the values
var refreshTimer = $interval(function () { refreshApp($scope.app.id); }, 5000); // call with inline function to avoid iteration argument passed see $interval docs
$scope.$on('$destroy', function () {
$interval.cancel(refreshTimer);
});
done();
});
});
});
+4 -4
View File
@@ -73,8 +73,8 @@
<div class="app-grid">
<div class="grid-item" ng-class="{ 'stopped': app.runState === 'stopped' }" ng-repeat="app in installedApps | selectedGroupAccessFilter:selectedGroup | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'" ng-class="{ 'admin-action': app.manifest.configurePath && (app | applicationLink) }">
<div class="grid-item-content" uib-tooltip="{{ app.fqdn }}">
<a ng-show="user.isAtLeastAdmin" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
<a ng-href="{{ app | applicationLink }}" ng-click="user.isAtLeastAdmin && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
<a ng-show="isOperator(app)" ng-href="#/app/{{ app.id}}/display" class="btn btn-lg btn-default grid-item-action"><i class="fas fa-cog"></i></a>
<a ng-href="{{ app | applicationLink }}" ng-click="isOperator(app) && (((app | installError) === true || (app | installationActive) === true) && showAppConfigure(app, 'repair')) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) || (app | installError) || (app | installationActive)}">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
@@ -88,7 +88,7 @@
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app | appProgressMessage }}">
{{ app | installationStateLabel }}
</div>
<div class="status" ng-style="{ 'visibility': user.isAtLeastAdmin && (app | installationActive) ? 'visible' : 'hidden' }">
<div class="status" ng-style="{ 'visibility': isOperator(app) && (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
@@ -96,7 +96,7 @@
</div>
</div>
<div class="usermanagement-indicator" ng-hide="user.isAtLeastAdmin">
<div class="usermanagement-indicator" ng-hide="isOperator(app)">
<i class="fas fa-user" ng-show="app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.sso' | tr }}" tooltip-placement="right"></i>
<i class="far fa-user" ng-show="!app.ssoAuth && !app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.nosso' | tr }}" tooltip-placement="right"></i>
<i class="fas fa-envelope" ng-show="app.manifest.addons.email" uib-tooltip="{{ 'apps.auth.email' | tr }}" tooltip-placement="right"></i>
+4
View File
@@ -77,6 +77,10 @@ angular.module('Application').controller('AppsController', ['$scope', '$translat
$location.path('/app/' + app.id + '/' + view);
};
$scope.isOperator = function (app) {
return app.accessLevel === 'operator' || app.accessLevel === 'admin';
};
Client.onReady(function () {
setTimeout(function () { $('#appSearch').focus(); }, 1);