You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datalab.apache.org by dg...@apache.org on 2020/12/23 14:26:13 UTC

[incubator-datalab] branch develop updated: [DATALAB-2156]: Added administration(configuration) page (#1017)

This is an automated email from the ASF dual-hosted git repository.

dgnatyshyn pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git


The following commit(s) were added to refs/heads/develop by this push:
     new 5e332ec  [DATALAB-2156]: Added administration(configuration) page (#1017)
5e332ec is described below

commit 5e332ec7eddb38cafa02fb2fd3daf4e3e8c6b859
Author: Dmytro Gnatyshyn <42...@users.noreply.github.com>
AuthorDate: Wed Dec 23 16:26:08 2020 +0200

    [DATALAB-2156]: Added administration(configuration) page (#1017)
    
    [DATALAB-2156]: Added administration(configuration) page
---
 services/billing-azure/billing.yml                 |   2 +-
 .../DynamicChangePropertiesException.java          |   8 +
 .../DynamicChangePropertiesExceptionMapper.java    |  20 ++
 .../datalab/backendapi/SelfServiceApplication.java |   2 +
 .../resources/admin/ChangePropertiesResource.java  | 116 +++++++++++
 .../datalab/backendapi/resources/dto/YmlDTO.java   |   9 +
 .../service/impl/DynamicChangeProperties.java      | 180 +++++++++++++++++
 .../src/main/resources/webapp/package-lock.json    |  19 ++
 .../src/main/resources/webapp/package.json         |   1 +
 .../app/administration/administration.module.ts    |   5 +-
 .../configuration/configuration.component.html     |  96 +++++++++
 .../configuration/configuration.component.scss     |  92 +++++++++
 .../configuration/configuration.component.ts       | 220 +++++++++++++++++++++
 .../src/app/administration/configuration/index.ts  |  47 +++++
 .../main/resources/webapp/src/app/app.module.ts    |   4 +-
 .../resources/webapp/src/app/app.routing.module.ts |   6 +
 .../resources/webapp/src/app/core/core.module.ts   |   2 +
 .../core/interceptors/http.token.interceptor.ts    |  13 +-
 .../src/app/core/pipes/keys-pipe/keys.pipe.ts      |   1 +
 .../services/applicationServiceFacade.service.ts   |  25 +++
 .../app/core/services/configutration.service.ts    |  59 ++++++
 .../webapp/src/app/core/util/checkUtils.ts         |  25 +--
 .../multi-level-select-dropdown.component.scss     |   2 +
 .../confirmation-dialog.component.ts               |   1 -
 .../src/app/shared/navbar/navbar.component.html    |   6 +
 .../webapp/src/assets/styles/_general.scss         |   1 +
 .../resources/webapp/src/assets/styles/_theme.scss |  22 +++
 .../src/main/resources/webapp/src/styles.scss      |   5 +-
 28 files changed, 959 insertions(+), 30 deletions(-)

diff --git a/services/billing-azure/billing.yml b/services/billing-azure/billing.yml
index 188bbe6..0695c08 100644
--- a/services/billing-azure/billing.yml
+++ b/services/billing-azure/billing.yml
@@ -89,4 +89,4 @@ datalab:
     database: datalabdb
   ssnStorageAccountTagName: <AZURE_SSN_STORAGE_ACCOUNT_TAG>
   sharedStorageAccountTagName: <AZURE_SHARED_STORAGE_ACCOUNT_TAG>
-  datalakeTagName: <AZURE_DATALAKE_TAG>
\ No newline at end of file
+  datalakeTagName: <AZURE_DATALAKE_TAG>
diff --git a/services/common/src/main/java/com/epam/datalab/exceptions/DynamicChangePropertiesException.java b/services/common/src/main/java/com/epam/datalab/exceptions/DynamicChangePropertiesException.java
new file mode 100644
index 0000000..68550ae
--- /dev/null
+++ b/services/common/src/main/java/com/epam/datalab/exceptions/DynamicChangePropertiesException.java
@@ -0,0 +1,8 @@
+package com.epam.datalab.exceptions;
+
+public class DynamicChangePropertiesException extends DatalabException {
+
+    public DynamicChangePropertiesException(String message) {
+        super(message);
+    }
+}
diff --git a/services/datalab-webapp-common/src/main/java/com/epam/datalab/rest/mappers/DynamicChangePropertiesExceptionMapper.java b/services/datalab-webapp-common/src/main/java/com/epam/datalab/rest/mappers/DynamicChangePropertiesExceptionMapper.java
new file mode 100644
index 0000000..6408b16
--- /dev/null
+++ b/services/datalab-webapp-common/src/main/java/com/epam/datalab/rest/mappers/DynamicChangePropertiesExceptionMapper.java
@@ -0,0 +1,20 @@
+package com.epam.datalab.rest.mappers;
+
+import com.epam.datalab.exceptions.DynamicChangePropertiesException;
+import com.epam.datalab.rest.dto.ErrorDTO;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+public class DynamicChangePropertiesExceptionMapper implements ExceptionMapper<DynamicChangePropertiesException> {
+
+    @Override
+    public Response toResponse(DynamicChangePropertiesException e) {
+        final Response.Status status = Response.Status.NO_CONTENT;
+        return Response.status(status)
+                .type(MediaType.APPLICATION_JSON)
+                .entity(new ErrorDTO(status.getStatusCode(), e.getMessage()))
+                .build();
+    }
+}
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/SelfServiceApplication.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/SelfServiceApplication.java
index 65059f8..9c5f166 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/SelfServiceApplication.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/SelfServiceApplication.java
@@ -46,6 +46,7 @@ import com.epam.datalab.backendapi.resources.SystemInfoResource;
 import com.epam.datalab.backendapi.resources.UserGroupResource;
 import com.epam.datalab.backendapi.resources.UserRoleResource;
 import com.epam.datalab.backendapi.resources.UserSettingsResource;
+import com.epam.datalab.backendapi.resources.admin.ChangePropertiesResource;
 import com.epam.datalab.backendapi.resources.callback.BackupCallback;
 import com.epam.datalab.backendapi.resources.callback.CheckInactivityCallback;
 import com.epam.datalab.backendapi.resources.callback.ComputationalCallback;
@@ -198,6 +199,7 @@ public class SelfServiceApplication extends Application<SelfServiceApplicationCo
 	    jersey.register(injector.getInstance(ProjectCallback.class));
 	    jersey.register(injector.getInstance(OdahuResource.class));
 	    jersey.register(injector.getInstance(OdahuCallback.class));
+	    jersey.register(injector.getInstance(ChangePropertiesResource.class));
     }
 
     private void disableGzipHandlerForGuacamoleServlet(Server server) {
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/admin/ChangePropertiesResource.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/admin/ChangePropertiesResource.java
new file mode 100644
index 0000000..2386b86
--- /dev/null
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/admin/ChangePropertiesResource.java
@@ -0,0 +1,116 @@
+package com.epam.datalab.backendapi.resources.admin;
+
+import com.epam.datalab.auth.UserInfo;
+import com.epam.datalab.backendapi.resources.dto.YmlDTO;
+import com.epam.datalab.backendapi.roles.UserRoles;
+import com.epam.datalab.backendapi.service.impl.DynamicChangeProperties;
+import io.dropwizard.auth.Auth;
+import lombok.NoArgsConstructor;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+@Path("admin")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@NoArgsConstructor
+public class ChangePropertiesResource {
+
+    @GET
+    @Path("/self-service")
+    public Response getSelfServiceProperties(@Auth UserInfo userInfo) {
+        if (UserRoles.isAdmin(userInfo)) {
+            return Response
+                    .ok(DynamicChangeProperties.getSelfServiceProperties())
+                    .build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @GET
+    @Path("/provisioning-service")
+    public Response getProvisioningServiceProperties(@Auth UserInfo userInfo) {
+        if (UserRoles.isAdmin(userInfo)) {
+            return Response
+                    .ok(DynamicChangeProperties.getProvisioningServiceProperties())
+                    .build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @GET
+    @Path("/billing")
+    public Response getBillingServiceProperties(@Auth UserInfo userInfo) {
+        if (UserRoles.isAdmin(userInfo)) {
+            return Response
+                    .ok(DynamicChangeProperties.getBillingServiceProperties())
+                    .build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @POST
+    @Path("/self-service")
+    public Response overwriteSelfServiceProperties(@Auth UserInfo userInfo, YmlDTO ymlDTO) {
+        if (UserRoles.isAdmin(userInfo)) {
+            DynamicChangeProperties.overwriteSelfServiceProperties(ymlDTO.getYmlString());
+            return Response.ok().build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @POST
+    @Path("/provisioning-service")
+    public Response overwriteProvisioningServiceProperties(@Auth UserInfo userInfo, YmlDTO ymlDTO) {
+        if (UserRoles.isAdmin(userInfo)) {
+            DynamicChangeProperties.overwriteProvisioningServiceProperties(ymlDTO.getYmlString());
+            return Response.ok().build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @POST
+    @Path("/billing")
+    public Response overwriteBillingServiceProperties(@Auth UserInfo userInfo, YmlDTO ymlDTO) {
+        if (UserRoles.isAdmin(userInfo)) {
+            DynamicChangeProperties.overwriteBillingServiceProperties(ymlDTO.getYmlString());
+            return Response.ok().build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+
+    @POST
+    @Path("/restart")
+    public Response restart(@Auth UserInfo userInfo,
+                            @QueryParam("billing") boolean billing,
+                            @QueryParam("provserv") boolean provserv,
+                            @QueryParam("ui") boolean ui) {
+        if (UserRoles.isAdmin(userInfo)) {
+        DynamicChangeProperties.restart(billing, provserv, ui);
+        return Response.ok().build();
+        } else {
+            return Response
+                    .status(Response.Status.FORBIDDEN)
+                    .build();
+        }
+    }
+}
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/YmlDTO.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/YmlDTO.java
new file mode 100644
index 0000000..5317064
--- /dev/null
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/YmlDTO.java
@@ -0,0 +1,9 @@
+package com.epam.datalab.backendapi.resources.dto;
+
+import lombok.Data;
+
+@Data
+public class YmlDTO {
+
+    private String ymlString;
+}
\ No newline at end of file
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/DynamicChangeProperties.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/DynamicChangeProperties.java
new file mode 100644
index 0000000..9a7d349
--- /dev/null
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/DynamicChangeProperties.java
@@ -0,0 +1,180 @@
+package com.epam.datalab.backendapi.service.impl;
+
+import com.epam.datalab.exceptions.DynamicChangePropertiesException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Slf4j
+public class DynamicChangeProperties {
+
+    private static final String SELF_SERVICE = "self-service.yml";
+    private static final String SELF_SERVICE_PROP_PATH = "/opt/datalab/conf/self-service.yml";
+    private static final String SELF_SERVICE_SUPERVISORCTL_RUN_NAME = " ui ";
+    private static final String PROVISIONING_SERVICE = "provisioning.yml";
+    private static final String PROVISIONING_SERVICE_PROP_PATH = "/opt/datalab/conf/provisioning.yml";
+    private static final String PROVISIONING_SERVICE_SUPERVISORCTL_RUN_NAME = " provserv ";
+    private static final String BILLING_SERVICE = "billing.yml";
+    private static final String BILLING_SERVICE_PROP_PATH = "/opt/datalab/conf/billing.yml";
+    private static final String BILLING_SERVICE_SUPERVISORCTL_RUN_NAME = " billing ";
+    private static final String SECRET_REGEX = "(.*)[sS]ecret(.*): (.*)";
+    private static final String SECRET_REPLACEMENT_FORMAT = " ***********";
+    private static final String SH_COMMAND = "sudo supervisorctl restart";
+
+    private static final String LICENCE =
+            "# *****************************************************************************\n" +
+                    "#\n" +
+                    "#  Licensed to the Apache Software Foundation (ASF) under one\n" +
+                    "#  or more contributor license agreements.  See the NOTICE file\n" +
+                    "#  distributed with this work for additional information\n" +
+                    "#  regarding copyright ownership.  The ASF licenses this file\n" +
+                    "#  to you under the Apache License, Version 2.0 (the\n" +
+                    "#  \"License\"); you may not use this file except in compliance\n" +
+                    "#  with the License.  You may obtain a copy of the License at\n" +
+                    "#\n" +
+                    "#  http://www.apache.org/licenses/LICENSE-2.0\n" +
+                    "#\n" +
+                    "#  Unless required by applicable law or agreed to in writing,\n" +
+                    "#  software distributed under the License is distributed on an\n" +
+                    "#  \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n" +
+                    "#  KIND, either express or implied.  See the License for the\n" +
+                    "#  specific language governing permissions and limitations\n" +
+                    "#  under the License.\n" +
+                    "#\n" +
+                    "# ******************************************************************************";
+
+    private static final int DEFAULT_VALUE_PLACE = 1;
+    private static final int DEFAULT_NAME_PLACE = 0;
+
+    public static String getSelfServiceProperties() {
+        return readFileAsString(SELF_SERVICE_PROP_PATH, SELF_SERVICE);
+    }
+
+    public static String getProvisioningServiceProperties() {
+        return readFileAsString(PROVISIONING_SERVICE_PROP_PATH, PROVISIONING_SERVICE);
+    }
+
+    public static String getBillingServiceProperties() {
+        return readFileAsString(BILLING_SERVICE_PROP_PATH, BILLING_SERVICE);
+    }
+
+    public static void overwriteSelfServiceProperties(String ymlString) {
+        writeFileFromString(ymlString, SELF_SERVICE, SELF_SERVICE_PROP_PATH);
+    }
+
+    public static void overwriteProvisioningServiceProperties(String ymlString) {
+        writeFileFromString(ymlString, PROVISIONING_SERVICE, PROVISIONING_SERVICE_PROP_PATH);
+    }
+
+    public static void overwriteBillingServiceProperties(String ymlString) {
+        writeFileFromString(ymlString, BILLING_SERVICE, BILLING_SERVICE_PROP_PATH);
+    }
+
+    public static void restart(boolean billing, boolean provserv, boolean ui) {
+        try {
+            String shCommand = buildSHCommand(billing, provserv, ui);
+            log.info("Tying to restart ui: {},  provserv: {}, billing: {}, with command: {}", ui,
+                    provserv, billing, shCommand);
+            Runtime.getRuntime().exec(shCommand).waitFor();
+
+
+        } catch (IOException | InterruptedException e) {
+            log.error(e.getMessage());
+        }
+    }
+
+    private static String buildSHCommand(boolean billing, boolean provserv, boolean ui) {
+        StringBuilder stringBuilder = new StringBuilder(SH_COMMAND);
+        if (billing) stringBuilder.append(BILLING_SERVICE_SUPERVISORCTL_RUN_NAME);
+        if (provserv) stringBuilder.append(PROVISIONING_SERVICE_SUPERVISORCTL_RUN_NAME);
+        if (ui) stringBuilder.append(SELF_SERVICE_SUPERVISORCTL_RUN_NAME);
+        return stringBuilder.toString();
+    }
+
+    private static String readFileAsString(String selfServicePropPath, String serviceName) {
+        try {
+            log.trace("Trying to read self-service.yml, file from path {} :", selfServicePropPath);
+            String currentConf = FileUtils.readFileToString(new File(selfServicePropPath), Charset.defaultCharset());
+            return hideSecretsAndRemoveLicence(currentConf);
+        } catch (IOException e) {
+            log.error(e.getMessage());
+            throw new DynamicChangePropertiesException(String.format("Failed while read file %s", serviceName));
+        }
+    }
+
+    private static String hideSecretsAndRemoveLicence(String currentConf) {
+        Matcher m = Pattern.compile(SECRET_REGEX).matcher(currentConf);
+        List<String> secrets = new ArrayList<>();
+        String confWithReplacedSecretConf = removeLicence(currentConf);
+        while (m.find()) {
+            secrets.add(m.group().split(":")[DEFAULT_VALUE_PLACE]);
+        }
+        for (String secret : secrets) {
+            confWithReplacedSecretConf = confWithReplacedSecretConf.replace(secret, SECRET_REPLACEMENT_FORMAT);
+        }
+        return confWithReplacedSecretConf;
+    }
+
+    private static String removeLicence(String conf) {
+        return conf.substring(LICENCE.length() + 7);
+    }
+
+    private static void writeFileFromString(String newPropFile, String serviceName, String servicePath) {
+        try {
+            String oldFile = FileUtils.readFileToString(new File(servicePath), Charset.defaultCharset());
+            BufferedWriter writer = new BufferedWriter(new FileWriter(servicePath));
+            log.trace("Trying to overwrite {}, file for path {} :", serviceName, servicePath);
+            writer.write(addLicence());
+            writer.write(checkAndReplaceSecretIfEmpty(newPropFile, oldFile));
+            log.info("{} overwritten successfully", serviceName);
+            writer.close();
+        } catch (IOException e) {
+            log.error("Failed during overwriting {}", serviceName);
+            throw new DynamicChangePropertiesException(String.format("Failed during overwriting %s", serviceName));
+        }
+    }
+
+    private static String addLicence() {
+        return LICENCE + "\n\n";
+    }
+
+    private static String checkAndReplaceSecretIfEmpty(String newPropFile, String oldProf) {
+        Map<String, String> emptySecrets = findEmptySecret(newPropFile);
+        return emptySecrets.isEmpty() ? newPropFile : replaceEmptySecret(newPropFile, oldProf, emptySecrets);
+    }
+
+    private static String replaceEmptySecret(String newPropFile, String oldProf, Map<String, String> emptySecrets) {
+        String fileWithReplacedEmptySecrets = newPropFile;
+        Matcher oldProfMatcher = Pattern.compile(SECRET_REGEX).matcher(oldProf);
+        while (oldProfMatcher.find()) {
+            String[] s = oldProfMatcher.group().split(":");
+            if (emptySecrets.containsKey(s[DEFAULT_NAME_PLACE])) {
+                fileWithReplacedEmptySecrets = fileWithReplacedEmptySecrets.replace(emptySecrets.get(s[DEFAULT_NAME_PLACE]), oldProfMatcher.group());
+            }
+        }
+        return fileWithReplacedEmptySecrets;
+    }
+
+    private static Map<String, String> findEmptySecret(String newPropFile) {
+        Matcher newPropFileMatcher = Pattern.compile(SECRET_REGEX).matcher(newPropFile);
+        Map<String, String> emptySecrets = new HashMap<>();
+        while (newPropFileMatcher.find()) {
+            String[] s = newPropFileMatcher.group().split(":");
+            if (s[DEFAULT_VALUE_PLACE].equals(SECRET_REPLACEMENT_FORMAT)) {
+                emptySecrets.put(s[DEFAULT_NAME_PLACE], newPropFileMatcher.group());
+            }
+        }
+        return emptySecrets;
+    }
+}
diff --git a/services/self-service/src/main/resources/webapp/package-lock.json b/services/self-service/src/main/resources/webapp/package-lock.json
index a4d8a2f..89cca19 100644
--- a/services/self-service/src/main/resources/webapp/package-lock.json
+++ b/services/self-service/src/main/resources/webapp/package-lock.json
@@ -2002,6 +2002,11 @@
         "negotiator": "0.6.2"
       }
     },
+    "ace-builds": {
+      "version": "1.4.12",
+      "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.4.12.tgz",
+      "integrity": "sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg=="
+    },
     "acorn": {
       "version": "6.4.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz",
@@ -2602,6 +2607,11 @@
       "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
       "dev": true
     },
+    "brace": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz",
+      "integrity": "sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg="
+    },
     "brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -7092,6 +7102,15 @@
         "date-fns": "^1.29.0"
       }
     },
+    "ng2-ace-editor": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/ng2-ace-editor/-/ng2-ace-editor-0.3.9.tgz",
+      "integrity": "sha512-e8Q4YCirlL/OEiekewmzupG+zV3prYsiYmQnRzQzd0wNgsPjOLOdb0it7cCbzFfIXKGyIIHKTW5584WxPr2LnQ==",
+      "requires": {
+        "ace-builds": "^1.4.2",
+        "brace": "^0.11.1"
+      }
+    },
     "ngx-toastr": {
       "version": "12.1.0",
       "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-12.1.0.tgz",
diff --git a/services/self-service/src/main/resources/webapp/package.json b/services/self-service/src/main/resources/webapp/package.json
index 6baa7a2..bd320b3 100644
--- a/services/self-service/src/main/resources/webapp/package.json
+++ b/services/self-service/src/main/resources/webapp/package.json
@@ -33,6 +33,7 @@
     "moment": "^2.24.0",
     "moment-timezone": "^0.5.31",
     "ng-daterangepicker": "^1.1.0",
+    "ng2-ace-editor": "^0.3.9",
     "ngx-toastr": "^12.1.0",
     "rxjs": "^6.6.3",
     "rxjs-compat": "6.5.3",
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
index 6d5b6d0..dfbbbf7 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
@@ -23,11 +23,12 @@ import { CommonModule } from '@angular/common';
 import { ManagenementModule } from './management';
 import { ProjectModule } from './project';
 import { RolesModule } from './roles';
+import {ConfigurationModule} from './configuration';
 import {OdahuModule} from './odahu';
 
 @NgModule({
-  imports: [CommonModule, ManagenementModule, ProjectModule, RolesModule, OdahuModule],
+  imports: [CommonModule, ManagenementModule, ProjectModule, RolesModule, ConfigurationModule, OdahuModule],
   declarations: [],
-  exports: [ManagenementModule, ProjectModule, RolesModule, OdahuModule]
+  exports: [ManagenementModule, ProjectModule, RolesModule, ConfigurationModule, OdahuModule]
 })
 export class AdministrationModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.html
new file mode 100644
index 0000000..8aab7c8
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.html
@@ -0,0 +1,96 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+<div class="base-retreat">
+  <div class="sub-nav">
+    <button mat-raised-button class="butt"
+            (click)="action('save')"
+            [disabled]="!services['provisioning-service'].isConfigChanged && !services['self-service'].isConfigChanged && !services['billing'].isConfigChanged"
+    >
+      Save
+    </button>
+    <button mat-raised-button class="butt"
+            (click)="action('discard')"
+            [disabled]="!services['provisioning-service'].isConfigChanged && !services['self-service'].isConfigChanged && !services['billing'].isConfigChanged"
+    >
+      Discard changes
+    </button>
+    <button mat-raised-button class="butt" (click)="refreshConfig()">
+      <i class="material-icons refresh-icon">autorenew</i>Refresh
+    </button>
+  </div>
+  <mat-divider></mat-divider>
+  <div class="configuration-wrapper">
+    <mat-tab-group animationDuration="0.5ms" (selectedTabChange)="tabChanged($event)">
+      <mat-tab label="Main"
+               [disabled]="!(!services['provisioning-service'].isConfigChanged && !services['self-service'].isConfigChanged && !services['billing'].isConfigChanged) && activeTab.index !== 0"
+      >
+        <h4>Main settings</h4>
+        <div class="main-wrapper">
+          <section class="section">
+            <p class="section-title">Restart services</p>
+            <div class="section-content">
+              <ul class="list-menu selection-list">
+                <li *ngFor="let service of services | keys">
+                  <p class="list-item" role="menuitem">
+                    <span (click)="toggleSetings(service.key);$event.stopPropagation()" class="d-flex">
+                      <span class="empty-checkbox" [ngClass]="{'checked': services[service.key].selected}">
+                        <span class="checked-checkbox" *ngIf="services[service.key].selected"></span>
+                      </span>
+                      {{service.value.label}}
+                    </span>
+                  </p>
+                </li>
+              </ul>
+              <button mat-raised-button type="button" class="butt action" (click)="restartServices()">Restart</button>
+            </div>
+          </section>
+        </div>
+      </mat-tab>
+      <mat-tab label="Provisioning"
+               [disabled]="!(!services['provisioning-service'].isConfigChanged
+               && !services['self-service'].isConfigChanged
+               && !services['billing'].isConfigChanged)
+               && activeTab.index !== 1"
+      >
+        <h4>Edit provisioning.yml</h4>
+        <div class="editor-wrap">
+          <div ace-editor [(text)]="services['provisioning-service'].config" mode="yaml" (textChange)="configUpdate('provisioning-service')"></div>
+        </div>
+      </mat-tab>
+      <mat-tab label="Self service"
+               [disabled]="!(!services['provisioning-service'].isConfigChanged && !services['self-service'].isConfigChanged && !services['billing'].isConfigChanged) && activeTab.index !== 2"
+      >
+         <h4>Edit self-service.yml</h4>
+        <div class="editor-wrap">
+          <div ace-editor mode="yaml" [(text)]="services['self-service'].config" (textChange)="configUpdate('self-service')"></div>
+        </div>
+      </mat-tab>
+      <mat-tab label="Billing"
+               [disabled]="!(!services['provisioning-service'].isConfigChanged && !services['self-service'].isConfigChanged && !services['billing'].isConfigChanged) && activeTab.index !== 3"
+      >
+        <h4>Edit billing.yml</h4>
+        <div class="editor-wrap">
+          <div ace-editor [(text)]="services.billing.config" mode="yaml" (textChange)="configUpdate('billing')"></div>
+        </div>
+      </mat-tab>
+    </mat-tab-group>
+  </div>
+</div>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.scss
new file mode 100644
index 0000000..40f533a
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.scss
@@ -0,0 +1,92 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.configuration-wrapper{
+  box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
+  padding-top: 0;
+  height: calc(100vh - 130px);
+
+  .sub-nav{
+    justify-content: flex-end;
+    padding: 0 30px;
+  }
+
+  h4{
+    padding: 10px 30px;
+    color: rgba(0,0,0,.87);
+  }
+
+  .editor-wrap{
+    height: calc(100% - 60px);
+    box-shadow: 0 2px 24px 0 rgba(73,93,112,0.3);
+    margin: 0 30px;
+  }
+
+  .main-wrapper{
+    padding: 10px 30px;
+
+    .section{
+
+      &-title{
+        font-size: 17px;
+      }
+
+      &-content {
+
+        .list-menu {
+          width: 100%;
+          max-height: 450px;
+          left: 0;
+          padding: 10px 0;
+          margin: 0;
+          overflow-y: auto;
+          overflow-x: hidden;
+
+          li {
+            padding: 0;
+            margin: 0;
+          }
+
+          .list-item{
+            padding: 5px 10px;
+            display: flex;
+
+            span{
+              cursor: pointer;
+            }
+
+            .empty-checkbox{
+              display: block;
+              margin-right: 5px;
+              margin-top: 0;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+.sub-nav{
+  justify-content: flex-end;
+
+  .butt{
+    margin-left: 10px;
+  }
+
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.ts
new file mode 100644
index 0000000..5e0d1f9
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/configuration.component.ts
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {Component, OnInit, Inject, HostListener} from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import {HealthStatusService, AppRoutingService} from '../../core/services';
+import {MatTabChangeEvent} from '@angular/material/tabs';
+import {Router} from '@angular/router';
+import {ConfigurationService} from '../../core/services/configutration.service';
+import 'brace';
+import 'brace/mode/yaml';
+
+@Component({
+  selector: 'datalab-configuration',
+  templateUrl: './configuration.component.html',
+  styleUrls: ['./configuration.component.scss']
+})
+export class ConfigurationComponent implements OnInit {
+  private healthStatus: any;
+  public activeTab = {index: 0};
+  public activeService: string;
+  public services = {
+    'self-service': {label: 'Self service', selected: false, config: '', serverConfig: '', isConfigChanged: false},
+    'provisioning-service': {label: 'Provisioning service', selected: false, config: '', serverConfig: '', isConfigChanged: false},
+    'billing': {label: 'Billing', selected: false, config: '', serverConfig: '', isConfigChanged: false},
+  };
+
+  private confirmMessages = {
+    restartService: 'Restarting services will make DataLab unavailable for some time.',
+    discardChanges: 'Discard all unsaved changes.',
+    saveChanges: 'After you save changes you need to restart service.',
+  };
+
+  @HostListener('window:keydown', ['$event'])
+  onKeyDown(event: KeyboardEvent) {
+    if ((event.metaKey || event.ctrlKey) &&
+      event.key === 's' &&
+      this.activeTab.index !== 0 &&
+      this.router.url === '/configuration' &&
+      this.services[this.activeService].config !== this.services[this.activeService].serverConfig
+    ) {
+      this.action('save');
+      event.preventDefault();
+    }
+  }
+
+  constructor(
+    private healthStatusService: HealthStatusService,
+    private appRoutingService: AppRoutingService,
+    private configurationService: ConfigurationService,
+    private router: Router,
+    public dialog: MatDialog
+  ) { }
+
+  ngOnInit() {
+    this.getEnvironmentHealthStatus();
+    this.getServicesConfig(...Object.keys(this.services));
+  }
+
+  private getEnvironmentHealthStatus() {
+    this.healthStatusService.getEnvironmentHealthStatus()
+      .subscribe((result: any) => {
+          this.healthStatus = result;
+          !this.healthStatus.admin && !this.healthStatus.projectAdmin && this.appRoutingService.redirectToHomePage();
+          }
+      );
+  }
+
+  public refreshConfig() {
+    this.getServicesConfig(...Object.keys(this.services));
+  }
+
+  public action(action) {
+    this.dialog.open(SettingsConfirmationDialogComponent, { data: {
+        action: action, message: action === 'discard' ? this.confirmMessages.discardChanges : this.confirmMessages.saveChanges
+      }, panelClass: 'modal-sm' })
+      .afterClosed().subscribe(result => {
+      if (result && action === 'save') this.setServiceConfig(this.activeService, this.services[this.activeService].config);
+      if (result && action === 'discard') this.services[this.activeService].config = this.services[this.activeService].serverConfig;
+      this.configUpdate(this.activeService);
+    });
+  }
+
+  private getServicesConfig(...services) {
+    services.forEach(service => {
+      this.configurationService.getServiceSettings(service).subscribe(config => {
+          this.services[service].config = config;
+          this.services[service].serverConfig = config;
+        this.configUpdate(service);
+        }
+      );
+    });
+    this.clearSelectedServices();
+  }
+
+  private setServiceConfig(service, config) {
+    this.configurationService.setServiceConfig(service, config).subscribe(res => {
+      this.getServicesConfig(service);
+      }
+    );
+  }
+
+  public tabChanged(tabChangeEvent: MatTabChangeEvent): void {
+    this.activeTab = tabChangeEvent;
+    if (this.activeTab.index === 1) {
+      this.activeService = 'provisioning-service';
+    } else if (this.activeTab.index === 2) {
+      this.activeService = 'self-service';
+    } else if (this.activeTab.index === 3) {
+      this.activeService = 'billing';
+    } else {
+      this.activeService = '';
+    }
+
+    if (!!this.activeService) {
+      if (this.services[this.activeService].config !== this.services[this.activeService].serverConfig) {
+        this.dialog.open(SettingsConfirmationDialogComponent, { data: {
+            action: 'Was changed'
+          }, panelClass: 'modal-sm' })
+          .afterClosed().subscribe(result => {
+          if (result) {
+            this.services[this.activeService].serverConfig = this.services[this.activeService].config;
+          } else {
+            this.services[this.activeService].config = this.services[this.activeService].serverConfig;
+          }
+        });
+      }
+    }
+    this.clearSelectedServices();
+  }
+
+  private clearSelectedServices() {
+    Object.keys(this.services).forEach(service => this.services[service].selected = false);
+  }
+
+  public toggleSetings(service) {
+    this.services[service].selected = !this.services[service].selected;
+  }
+
+  public restartServices() {
+    this.dialog.open(SettingsConfirmationDialogComponent, { data: {
+        action: 'Restart services', message: this.confirmMessages.restartService
+      }, panelClass: 'modal-sm' })
+      .afterClosed().subscribe(result => {
+      this.configurationService.restartServices(this.services['self-service'].selected,
+        this.services['provisioning-service'].selected,
+        this.services['billing'].selected
+      )
+        .subscribe(res => {
+          this.clearSelectedServices();
+        }
+      );
+
+    });
+  }
+
+  public configUpdate(service: string) {
+    this.services[service].isConfigChanged = this.services[service].config !== this.services[service].serverConfig;
+  }
+}
+
+@Component({
+  selector: 'confirm-dialog',
+  template: `
+  <div id="dialog-box">
+    <div class="dialog-header">
+      <h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> <span *ngIf="data.action === 'save' || data.action === 'discard'"> changes</span></h4>
+      <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+    </div>
+
+    <div mat-dialog-content class="content">
+      {{data.message}}
+    </div>
+    <div class="text-center ">
+      <p class="strong">Do you want to proceed?</p>
+    </div>
+    <div class="text-center m-top-20 pb-25">
+      <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+      <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+    </div>
+  </div>
+  `,
+  styles: [
+    `
+      .content { color: #718ba6; padding: 20px 50px; font-size: 14px; font-weight: 400; margin: 0; }
+      header { display: flex; justify-content: space-between; color: #607D8B; }
+      header h4 i { vertical-align: bottom; }
+      header a i { font-size: 20px; }
+      header a:hover i { color: #35afd5; cursor: pointer; }
+      .content{padding: 35px 30px 30px 30px;}
+      label{cursor: pointer}`
+  ]
+})
+
+export class SettingsConfirmationDialogComponent {
+  constructor(
+    public dialogRef: MatDialogRef<SettingsConfirmationDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+
+  }
+}
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/configuration/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/index.ts
new file mode 100644
index 0000000..369b8d7
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/configuration/index.ts
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
+import {InformMessageModule} from '../../shared/inform-message';
+import {ConfigurationComponent, SettingsConfirmationDialogComponent} from './configuration.component';
+import {AceEditorModule} from 'ng2-ace-editor';
+import {ConvertActionPipeModule} from '../../core/pipes/convert-action-pipe';
+import {KeysPipeModule} from '../../core/pipes/keys-pipe';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    MaterialModule,
+    FormControlsModule,
+    InformMessageModule,
+    AceEditorModule,
+    ConvertActionPipeModule,
+    KeysPipeModule
+  ],
+  declarations: [ConfigurationComponent, SettingsConfirmationDialogComponent],
+  entryComponents: [SettingsConfirmationDialogComponent],
+  exports: [ConfigurationComponent]
+})
+export class ConfigurationModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/app.module.ts b/services/self-service/src/main/resources/webapp/src/app/app.module.ts
index 9559cf1..e6e887c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/app.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/app.module.ts
@@ -41,6 +41,7 @@ import { CoreModule } from './core/core.module';
 import { SwaggerAPIModule } from './swagger';
 import {ReportsModule} from './reports/reports.module';
 import {LocalizationService} from './core/services/localization.service';
+import {AceEditorModule} from 'ng2-ace-editor';
 
 LocalizationService.registerCulture(window.navigator.language);
 
@@ -66,7 +67,8 @@ LocalizationService.registerCulture(window.navigator.language);
     RouterModule,
     AppRoutingModule,
     CoreModule.forRoot(),
-    ToastrModule.forRoot({ timeOut: 10000 })
+    ToastrModule.forRoot({ timeOut: 10000 }),
+    AceEditorModule
   ],
   providers: [{
     provide: LocationStrategy,
diff --git a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
index 461c5f7..4b7d06c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
@@ -34,6 +34,7 @@ import { RolesComponent } from './administration/roles/roles.component';
 import { SwaggerComponent } from './swagger/swagger.component';
 import { AuthorizationGuard, CheckParamsGuard, CloudProviderGuard, AdminGuard, AuditGuard } from './core/services';
 import {AuditComponent} from './reports/audit/audit.component';
+import {ConfigurationComponent} from './administration/configuration/configuration.component';
 import {OdahuComponent} from './administration/odahu/odahu.component';
 
 const routes: Routes = [{
@@ -74,6 +75,11 @@ const routes: Routes = [{
       component: ManagementComponent,
       canActivate: [AuthorizationGuard, AdminGuard]
     }, {
+      path: 'configuration',
+      component: ConfigurationComponent,
+      canActivate: [AuthorizationGuard, AdminGuard]
+    },
+    {
       path: 'swagger',
       component: SwaggerComponent,
       canActivate: [AuthorizationGuard]
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
index ae6221a..3cfd504 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
@@ -48,6 +48,7 @@ import { NoCacheInterceptor } from './interceptors/nocache.interceptor';
 import { ErrorInterceptor } from './interceptors/error.interceptor';
 
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import {ConfigurationService} from './services/configutration.service';
 import {AuditGuard, OdahuDeploymentService} from './services';
 
 @NgModule({
@@ -85,6 +86,7 @@ export class CoreModule {
         ProjectService,
         EndpointService,
         UserAccessKeyService,
+        ConfigurationService,
         OdahuDeploymentService,
 
         { provide: MatDialogRef, useValue: {} },
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts b/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
index 9d72d0b..5b6309a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/interceptors/http.token.interceptor.ts
@@ -18,7 +18,7 @@
  */
 
 import { Injectable } from '@angular/core';
-import { StorageService } from '../services/storage.service';
+import { StorageService } from '../services';
 import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
 
 import { Observable } from 'rxjs';
@@ -33,16 +33,13 @@ import { Observable } from 'rxjs';
     if (token)
       headersConfig['Authorization'] = `Bearer ${token}`;
 
-    // if (request.url.indexOf('api/bucket') !== -1) {
-    //   headersConfig['Authorization'] = `Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJfSEVvZmliX2djZXJpcllidE51dVBoSk81OEJNOFc5M1dHZW9VR3hTR2l3In0.eyJqdGkiOiIxY2E4OTQ1OS02MDU5LTQzOTctYTZhMy1kMzY5YTY0OTkyNzIiLCJleHAiOjE1ODc5OTUyNjgsIm5iZiI6MCwiaWF0IjoxNTg3OTk0OTY4LCJpc3MiOiJodHRwczovL2lkcC5kZW1vLmRsYWJhbmFseXRpY3MuY29tL2F1dGgvcmVhbG1zL2RsYWIiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNWIwYWEwMmYtYTU3ZS00MGM0LTk4ODQtNDlmYmU5OGViMzU4IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoib2Z1a3MtMTMwNC11aSIsIm [...]
-    // }
-
     if (!request.headers.has('Content-Type')
       && !request.headers.has('Upload')
       && request.url.indexOf('upload') === -1
-      && request.url.indexOf('download') === -1)
-
-      headersConfig['Content-Type'] = 'application/json; charset=UTF-8';
+      && request.url.indexOf('download') === -1
+      && request.url.indexOf('admin') === -1
+    )
+    headersConfig['Content-Type'] = 'application/json; charset=UTF-8';
 
     const header = request.clone({ setHeaders: headersConfig });
     return next.handle(header);
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/pipes/keys-pipe/keys.pipe.ts b/services/self-service/src/main/resources/webapp/src/app/core/pipes/keys-pipe/keys.pipe.ts
index 8c6fb0b..62bfc9c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/pipes/keys-pipe/keys.pipe.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/keys-pipe/keys.pipe.ts
@@ -27,6 +27,7 @@ export class KeysPipe implements PipeTransform {
     for (const key in value) {
       keys.push({ key: key, value: value[key]});
     }
+
     return keys;
   }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
index 97bbfd4..86374b0 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
@@ -79,6 +79,7 @@ export class ApplicationServiceFacade {
   private static readonly ENDPOINT = 'endpoint';
   private static readonly ENDPOINT_CONNECTION = 'endpoint_connection';
   private static readonly AUDIT = 'audit';
+  private static readonly ADMIN = 'admin';
   private static readonly QUOTA = 'quota';
 
   private requestRegistry: Dictionary<string>;
@@ -673,6 +674,27 @@ export class ApplicationServiceFacade {
       data);
   }
 
+  public buildGetServiceConfig(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.GET,
+      this.requestRegistry.Item(ApplicationServiceFacade.ADMIN),
+      data, { responseType: 'text' });
+  }
+
+  public buildSetServiceConfig(data, body): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.ADMIN) + data,
+      body, {
+        // responseType: 'text',
+        // headers: { 'Content-Type': 'text/plain' }
+      });
+  }
+
+  public buildRestartServices(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.ADMIN) + 'restart' + data,
+      null );
+  }
+
   private setupRegistry(): void {
     this.requestRegistry = new Dictionary<string>();
 
@@ -759,6 +781,9 @@ export class ApplicationServiceFacade {
 
     // audit
     this.requestRegistry.Add(ApplicationServiceFacade.AUDIT, '/api/audit');
+
+    // configuration
+    this.requestRegistry.Add(ApplicationServiceFacade.ADMIN, '/api/admin/');
   }
 
   private buildRequest(method: HTTPMethod, url_path: string, body: any, opt?) {
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/configutration.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/configutration.service.ts
new file mode 100644
index 0000000..9826bbd
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/configutration.service.ts
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from '@angular/core';
+import {Observable, of} from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+
+import { ApplicationServiceFacade } from './applicationServiceFacade.service';
+import { ErrorUtils } from '../util';
+
+@Injectable()
+export class ConfigurationService {
+  constructor(private applicationServiceFacade: ApplicationServiceFacade) { }
+
+  public getServiceSettings(service): Observable<{}> {
+     return this.applicationServiceFacade
+      .buildGetServiceConfig(service)
+       .pipe(
+      map(response => response),
+      catchError(ErrorUtils.handleServiceError));
+    }
+
+  public setServiceConfig(service, config) {
+    const settings = {
+      ymlString: config
+    };
+    return this.applicationServiceFacade
+      .buildSetServiceConfig(service, settings)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public restartServices(self, prov, billing) {
+    const queryString = `?billing=${billing}&provserv=${prov}&ui=${self}`;
+    return this.applicationServiceFacade
+      .buildRestartServices(queryString)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
index 4cd39c3..2cd1d01 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
@@ -20,6 +20,14 @@
 import { PATTERNS } from './patterns';
 
 export class CheckUtils {
+
+  public static endpointStatus = {
+    CREATING: 'CONNECTING',
+    STARTING: 'CONNECTING',
+    RUNNING: 'CONNECTED',
+    STOPPING: 'DISCONNECTING',
+    STOPPED: 'DISCONNECTED'
+  };
   public static isJSON(str) {
     try {
       JSON.parse(str);
@@ -42,26 +50,11 @@ export class CheckUtils {
 
   public static numberOnly(event): boolean {
     const charCode = (event.which) ? event.which : event.keyCode;
-    if (charCode > 31 && (charCode < 48 || charCode > 57)) {
-      return false;
-    }
-    return true;
+    return !(charCode > 31 && (charCode < 48 || charCode > 57));
   }
 
   public static delimitersFiltering(resource): string {
     return resource.replace(RegExp(PATTERNS.delimitersRegex, 'g'), '').toString().toLowerCase();
   }
 
-  public static decodeUnicode(str) {
-    str = str.replace(/\\/g, "%");
-    return unescape(str);
-  }
-
-  public static endpointStatus = {
-    CREATING: 'CONNECTING',
-    STARTING: 'CONNECTING',
-    RUNNING: 'CONNECTED',
-    STOPPING: 'DISCONNECTING',
-    STOPPED: 'DISCONNECTED'
-  }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss
index aab2b07..27e1e90 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss
@@ -120,9 +120,11 @@
       padding: 0;
       margin: 0;
     }
+
     .role-item{
       padding-left: 30px;
     }
+
     .role-cloud-item{
       padding-left: 60px;
     }
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.ts
index 2a00a6c..a89d40f 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.ts
@@ -60,7 +60,6 @@ export class ConfirmationDialogComponent implements OnInit {
   }
 
   ngOnInit() {
-    console.log(this.data);
     if (this.data.type !== 5) {
       this.confirmationType = this.data.type;
       this.notebook = this.data.notebook;
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
index 89de5e0..8a64a8c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
@@ -99,6 +99,12 @@
               <span *ngIf="isExpanded; else env">Environment Management</span>
               <ng-template #env><i class="material-icons">settings</i></ng-template>
             </a>
+            <a class="sub-nav-item" [style.margin-left.px]="isExpanded ? '30' : '0'"
+               [routerLink]="['/configuration']" [routerLinkActive]="['active']"
+               [routerLinkActiveOptions]="{exact:true}">
+              <span *ngIf="isExpanded; else env">Configuration</span>
+              <ng-template #env><i class="material-icons">settings</i></ng-template>
+            </a>
           </a>
           <a class="nav-item has-children" *ngIf="healthStatus?.billingEnabled || healthStatus?.auditEnabled">
             <span *ngIf="isExpanded">Reports</span>
diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_general.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_general.scss
index 9234c6a..e27d33f 100644
--- a/services/self-service/src/main/resources/webapp/src/assets/styles/_general.scss
+++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_general.scss
@@ -39,6 +39,7 @@ body.modal-open {
 .pr-3{padding-right: 3px}
 
 .pb-50 {padding-bottom: 50px;}
+.pb-25 {padding-bottom: 25px;}
 .pb-10 {padding-bottom: 10px;}
 
 .txt-r {text-align: right }
diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
index fc3887c..73ed337 100644
--- a/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
+++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
@@ -1026,6 +1026,28 @@ mat-progress-bar {
     }
   }
 }
+.configuration-wrapper{
+  box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
+  .mat-tab-header{
+    margin: 0 30px;
+  }
+  .mat-tab-label{
+    .mat-tab-label-content{
+      font-family: "Open Sans", sans-serif;
+      font-weight: 600;
+      font-size: 13px;
+    }
+  }
+
+  .ace-monokai{
+    height: calc(100%);
+  }
+
+  .ace_scrollbar{
+
+  }
+}
+
 
 
 
diff --git a/services/self-service/src/main/resources/webapp/src/styles.scss b/services/self-service/src/main/resources/webapp/src/styles.scss
index c09ccd9..3c5d948 100644
--- a/services/self-service/src/main/resources/webapp/src/styles.scss
+++ b/services/self-service/src/main/resources/webapp/src/styles.scss
@@ -382,12 +382,13 @@ input[type='number'] {
     text-align: center;
   }
 }
-#scrolling, .scrolling{
+#scrolling, .scrolling, ace_scrollbar{
   scrollbar-width: thin;
 }
 
 #scrolling::-webkit-scrollbar,
 .scrolling::-webkit-scrollbar,
+.ace_scrollbar::-webkit-scrollbar,
 .list-selected mat-chip-list .mat-chip-list-wrapper::-webkit-scrollbar {
   width: 5px;
   height: 5px;
@@ -395,6 +396,7 @@ input[type='number'] {
 
 #scrolling::-webkit-scrollbar-track,
 .scrolling::-webkit-scrollbar-track,
+.ace_scrollbar::-webkit-scrollbar-track,
 .list-selected mat-chip-list .mat-chip-list-wrapper::-webkit-scrollbar-track {
   box-shadow: none;
   -webkit-box-shadow: none;
@@ -403,6 +405,7 @@ input[type='number'] {
 
 #scrolling::-webkit-scrollbar-thumb,
 .scrolling::-webkit-scrollbar-thumb,
+.ace_scrollbar::-webkit-scrollbar-thumb,
 .list-selected mat-chip-list .mat-chip-list-wrapper::-webkit-scrollbar-thumb {
   background-color: #f6fafe;
   background-color: rgba(0, 0, 0, 0.4);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datalab.apache.org
For additional commands, e-mail: commits-help@datalab.apache.org